在R中安全地评估算术表达式?

12

编辑

好的,由于似乎有很多混淆,我将简化问题。您可以尝试回答下面的原始问题,或者您可以尝试回答下面的问题并忽略下面的所有内容。

我的目标是在极其受限制的环境中获取任意表达式并对其进行评估。此环境仅包含具有以下类型值的变量:

  • 数字向量
  • 仅接受一个或多个数字向量并返回数字向量的纯函数(即算术运算符)

此外,表达式必须能够使用任何字面量,例如数字和字符串常量(但不能使用数字或字符串向量,因为这些将需要 c)。我想在此环境中评估表达式,并确保表达式无法访问环境之外的任何内容,以便我可以确信评估表达式不会构成安全风险。因此,在下面的代码中,您能否填写一个字符串,该字符串在评估时会执行一些不良操作?“不良操作”被定义为向屏幕打印某些内容、访问变量 secret 的值、执行任何 shell 命令(最好是产生输出的命令),或者您认为其他任何不良操作(请解释您的选择)。

a <- 1
b <- 2
x <- 5
y <- 1:10
z <- -1

## Give secret a random value so that you can't just compute it from
## the above variables
secret <- rnorm(5)

allowed.variables <- c(
    ## Numeric variables
    "a", "b", "x", "y", "z",
    ## Arithmetic operators
    "(", "+", "-", "/", "*", "^", "sqrt", "log", "log10", "log2", "exp", "log1p")

restricted.environment <- Map(get, allowed.variables)

## Example naughty expressions that my method successfully guards
## against
expr1 <- "secret"
expr2 <- "cat('Printing something with cat\n')"
expr3 <- "system('echo Printing something via shell command')"

arbitrary.expression <- "?????????" # Your naughty string constant here

eval(parse(text=arbitrary.expression), envir=restricted.environment, enclos=emptyenv())

原始问题

我正在编写一些代码来接受用户输入的算术表达式并对其进行求值。我有一组特定的变量可以使用,并且有一个算术函数白名单(+-*/^等)。是否有任何方法可以评估一个表达式,以便只有这些变量和运算符在作用域内,以避免任意代码注入的可能性?我有一些可以工作的东西,但除非我确信它确实是防弹的,否则我不想真正使用它:

## Shortcut for parse-then-eval pattern
evalparse <- function(expr, ...) eval(parse(text=expr), ...)

# I control these
arithmetic.operators <- Map(get, c("(", "+", "-", "/", "*", "^", "sqrt", "log", "log10", "log2", "exp", "log1p"))
vars <- list(a=1, b=2)
safe.envir <- c(vars, arithmetic.operators)

# Assume that these expressions are user input, e.g. from a web form.
nice.expr <- "a + b"
naughty.expr <- paste("cat('ARBITRARY R CODE INJECTION\n'); system('echo ARBITRARY SHELL COMMAND INJECTION');", nice.expr)

## NOT SAFE! Lookups outside env still possible.
evalparse(nice.expr, envir=safe.envir)
evalparse(naughty.expr, envir=safe.envir)

## Is this safe?
evalparse(nice.expr, envir=safe.envir, enclos=emptyenv())
evalparse(naughty.expr, envir=safe.envir, enclos=emptyenv())

如果你在R中运行上述代码,你会发现第一次执行naughty.expr时,它成功地执行了其有效负载。然而,在第二次使用enclose=emptyenv()时,只有访问变量ab和指定的算术运算符,因此有效负载无法执行。

那么,这种方法(即eval(..., envir=safeenv, enclos=emptyenv()))是否适用于接受实际用户输入的生产环境,或者我是否忽略了某些狡猾的方法来在受限环境中执行任意代码?


naughty.expr <- "(a+b)*(b+b+a)*(b^b^b+b+a)*((b^(a+b)*(b*b+a)*(b*b*b-a))+a)" - flodel
@RyanThompson:你能给个例子吗?你目前的例子只适用于表达式使用a和/或b的情况。顺便说一句,我不确定这是安全的(或者能否变得安全)。 - Joshua Ulrich
这就是想要的。我有一组固定的变量,它们的值都是数字,还有一组允许在这些变量上执行的操作,并且我想要接受用户提供的关于这些变量的算术表达式,计算结果并将其返回给用户。您可以更改示例,将任意数量的数字向量放入“vars”中。 - Ryan C. Thompson
是的,我正在使用3.0.1版本,在该版本中mget函数的envir参数有一个默认值。我已经编辑了我的代码,改用get函数。 - Ryan C. Thompson
如何防止用户进行函数重载?如何防止用户使用白名单中的术语定义自己的运算符?例如,看看sos包代码。很容易修改它,以便用户可以拥有一个"?+"运算符,并使该运算符执行“某些不好的事情”。我不是编程专家,但我敢打赌,白名单中的任何内容都可能被利用。 - Carl Witthoft
显示剩余10条评论
1个回答

15

我对定义安全函数和评估任意代码的环境有一些不同的方法,但这只是一些风格上的变化。只要safe_f中的所有函数都是安全的,即它们不允许你执行任意代码,这种技术就是可靠的。我相信列表中的函数是安全的,但你需要检查每个源代码才能确定。

safe_f <- c(
  getGroupMembers("Math"),
  getGroupMembers("Arith"),
  getGroupMembers("Compare"),
  "<-", "{", "("
)

safe_env <- new.env(parent = emptyenv())

for (f in safe_f) {
  safe_env[[f]] <- get(f, "package:base")
}

safe_eval <- function(x) {
  eval(substitute(x), env = safe_env)
}

# Can't access variables outside of that environment
a <- 1
safe_eval(a)    

# But you can create in that environment
safe_eval(a <- 2)
# And retrieve later
safe_eval(a)
# a in the global environment is not affected
a

# You can't access dangerous functions
safe_eval(cat("Hi!"))

# And because function isn't included in the safe list
# you can't even create functions
safe_eval({
  log <- function() {
    stop("Danger!")
  }
  log()
})

这比rapporter沙盒要简单得多,因为你不是在尝试创建一个有用的R环境,只是一个有用的计算器环境,需要检查的函数集要小得多。

1
谢谢。这正是我希望听到的。我担心可能有一些特殊的技巧,即使在当前搜索路径上没有它们,也可以访问任意环境中的值。 - Ryan C. Thompson
@RyanThompson,有很多技巧,但它们都依赖于您不允许的函数。 - hadley
是的,但我担心可能会有一些晦涩的语法元素,它们不需要任何函数,而只是语言的一部分。但我想这确实是真的,R语言的每个语法元素最终都只是一个函数调用的语法糖。 - Ryan C. Thompson
这非常酷。我需要了解如何在safe_env中过滤数据框。即使我将'['包含在允许函数列表中并将数据框'df'放入安全环境中,像safe_eval(df [1,1])这样的东西似乎不起作用...目标是让用户输入(主要是“比较”函数)指导数据框的过滤。 - Nathan Siemers
将 '[.data.frame' 明确添加到允许函数列表中解决了这个问题。 - Nathan Siemers

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接