I would like to let a user enter a string, for a formula for y
as a function of x
, e.g.
fn <- "x^2 + exp(3*x)"
I would then like to evaluate this expression at some points, e.g. for values of x <- 1:0.1:100
.
I know I can do:
x <- 1:0.1:100
y <- eval(parse(text = fn))
However, this leaves my computer open to attacks if the user supplies fn
which is not a formula
fn <- 'x; print("Your computer is compromised")'
Are there any alternative ways of achieving what I want to do?
Evaluate an expression represented by a String. The expression can contain parentheses, you can assume parentheses are well-matched. For simplicity, you can assume only binary operations allowed are +, -, *, and /. Arithmetic Expressions can be written in one of three forms:
When you assign a value to a symbolic variable, expressions containing the variable are not automatically evaluated. Instead, evaluate expressions by using subs. Define the expression y = x^2. The value of y is still x^2 instead of 4. If you change the value of x again, the value of y stays x^2.
Since the eval function is not capable of evaluating character strings, we need to convert the character class to the expression class first. We can do that with the parse function as follows: As before the evaluation of our expression results in the value 15. Looks good!
If we want to get the result of this expression (i.e. evaluate the expression), we can use the eval command as follows: As you can see, the eval function returned 15 (the result of 3 * 5) to the RStudio console. Expressions are often stored as R character string. Let’s create such a character string in R:
One of R's coolest features is that it can process its own language, so you can create a function white list and check expressions against it:
# Function to check if an expression is safe
is_safe_call <- function(text, allowed.funs) {
# Recursive function to evaluate each element in the user expression
safe_fun <- function(call, allowed.funs) {
if(is.call(call)) {
call.l <- as.list(call)
if(!is.name(call.l[[1L]]) || !as.character(call.l[[1L]]) %in% allowed.funs)
stop(as.character(call.l[[1L]]), " is not an allowed function")
lapply(tail(call.l, -1L), safe_fun, allowed.funs)
}
TRUE
}
# parse and call recursive function
expr <- parse(text=text)
if(length(expr) > 1L) stop("Only single R expressions allowed")
safe_fun(expr[[1L]], allowed.funs)
}
We then define a whitelist of allowable functions. It is very important that you are extremely careful about what you allow in here. In particular, if you allow parse
, eval
, or any functions with possible unpleasant side effects (system
, unlink
, etc.) you open your system wide for attack.
allowed.funs <- c("+", "exp", "*", "^", "-", "sqrt")
And we test:
is_safe_call("x^2 + exp(3*x)", allowed.funs)
## [1] TRUE
is_safe_call("x^2 - sqrt(3*x)", allowed.funs)
## [1] TRUE
is_safe_call("unlink('~', recursive=TRUE)", allowed.funs)
## Error in safe_fun(parse(text = text)[[1L]], allowed.funs) :
## unlink is not an allowed function
is_safe_call("x + sqrt(unlink('~', recursive=TRUE))", allowed.funs)
## Error in FUN(X[[i]], ...) : unlink is not an allowed function
is_safe_call('x; print("Your computer is compromised")')
## Error in is_safe_call("x; print(\"Your computer is compromised\")") :
## Only single R expressions allowed
No waranties express or implied in this. There may be a way to hack this that I haven't thought of, so don't put this on a public facing server without extensive scrutiny, but I think this might just work.
Note that if someone can provide an expression that somehow hacks parse
itself, then you can be compromised that way.
EDIT: Ben Bolker suggested a clever trick to try to hack this, but this function is robust to that:
is_safe_call("exp <- function(...) system(\"echo do bad stuff\")", allowed.funs)
## Error in safe_fun(expr[[1L]], allowed.funs) :
## <- is not an allowed function
allowed.funs <- c("+", "exp", "*", "^", "-", "sqrt", "<-")
is_safe_call("exp <- function(...) system(\"echo do bad stuff\")", allowed.funs)
## Error in FUN(X[[i]], ...) : function is not an allowed function
allowed.funs <- c("+", "exp", "*", "^", "-", "sqrt", "<-", "function")
is_safe_call("exp <- function(...) system(\"echo do bad stuff\")", allowed.funs)
## Error in FUN(X[[i]], ...) : system is not an allowed function
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With