One idea that comes to mind is to interpret the R expression manually and impose some of your own restrictions. Here's an example:
library(rlang)
interpret <- function(expr_str,
max_length = 32,
whitelist = c("/", "*", "sqrt", "c")) {
safer_eval <- function(expr) {
if (rlang::is_call(expr)) {
fn_name <- rlang::call_name(expr)
if (!fn_name %in% whitelist) stop("Disallowed function: ", fn_name)
do.call(get(fn_name, baseenv()), Map(safer_eval, rlang::call_args(expr)))
} else if (rlang::is_syntactic_literal(expr)) {
expr
} else {
stop("Unknown expression: ", expr)
}
}
stopifnot(length(expr_str) < max_length)
safer_eval(rlang::parse_expr(expr_str))
}
# > interpret("c(2*3, 12)")
# [1] 6 12
# > interpret("c(2*3, 12, system('rm /tmp/foo'))")
# Error in (function (expr, env) : Disallowed function: system
- If the input string is longer than
max_length, it throws an exception. This prevents people from submitting very large strings that could tie up your R process with parsing.
- Recognize only a subset of R functions. In particular, those named by
whitelist that exist in base. This prevents people from calling functions like base::system. It's important that these functions don't accept quoted arguments that are evaluated separately, since that could defeat the whitelist approach.
- Anything other than a function call or a syntactic literal throws the
Unknown expression exception, so variables are not resolved and can't be created.
Since functions can't be defined and loops can't be created, I don't think it would be possible to tie up R with a busy loop or infinite recursion, and so we don't need to impose a limit on running time. We wouldn't be able to do that anyway without forking a new R process, or without writing a more involved interpreter or VM that maintained its own code vector and argument stack.
One extension point you might consider is, instead of throwing Unknown expression, interpret could accept the input object as a parameter, and you could resolve symbols to members of the input object. This way, your interpreted programs could participate in reactivity.
In the course of researching this I was directed to https://github.com/jeroen/RAppArmor by a co-worker, but it looks much more involved to set up, but is probably the "complete" answer, particularly if you have control over your hosting environment and are on Linux.