我该如何创建一个安全的Lua沙盒?

78

因此,Lua似乎非常适合在我的应用程序中实现安全的“用户脚本”。

然而,大多数嵌入lua的示例似乎包括加载所有标准库,包括"io"和"package"。

因此,我可以从解释器中排除这些库,但即使基本库中也包括访问文件系统的函数“dofile”和“loadfile”。

如何删除/阻止任何不安全的函数(如这些),而不仅仅是得到一个甚至没有基本功能如“ipairs”的解释器?

7个回答

54

你可以通过setfenv函数来设置运行不受信任代码的函数环境。以下是一个概述:

local env = {ipairs}
setfenv(user_script, env)
pcall(user_script)

user_script函数只能访问其环境中的内容。您可以显式添加要授予不受信任代码访问权限的函数(白名单)。在本例中,用户脚本仅能访问ipairs,而无法访问其他函数(如dofileloadfile等)。

有关Lua沙盒和更多信息,请参见Lua Sandboxes


18
注意,我认为应该是local env = {ipairs=ipairs}。如果你在交互式的lua命令行上运行这个程序,把整个代码包含在一个“do ... end”循环中,这样你就不会失去局部变量。 - BMitch
9
需要翻译的内容:It should be noted that this is the Lua 5.1 way of doing things.这是Lua 5.1的做法,需要注意。 - John K
在 Lua 5.2 及以上版本中,您应该使用带有 env 参数的 load() 来代替 setfenv(),因为它更安全,而且不容易被忘记。 - DarkWiiPlayer

35

这里是针对Lua 5.2的解决方案(包括一个示例环境,该环境也适用于5.1):

-- save a pointer to globals that would be unreachable in sandbox
local e=_ENV

-- sample sandbox environment
sandbox_env = {
  ipairs = ipairs,
  next = next,
  pairs = pairs,
  pcall = pcall,
  tonumber = tonumber,
  tostring = tostring,
  type = type,
  unpack = unpack,
  coroutine = { create = coroutine.create, resume = coroutine.resume, 
      running = coroutine.running, status = coroutine.status, 
      wrap = coroutine.wrap },
  string = { byte = string.byte, char = string.char, find = string.find, 
      format = string.format, gmatch = string.gmatch, gsub = string.gsub, 
      len = string.len, lower = string.lower, match = string.match, 
      rep = string.rep, reverse = string.reverse, sub = string.sub, 
      upper = string.upper },
  table = { insert = table.insert, maxn = table.maxn, remove = table.remove, 
      sort = table.sort },
  math = { abs = math.abs, acos = math.acos, asin = math.asin, 
      atan = math.atan, atan2 = math.atan2, ceil = math.ceil, cos = math.cos, 
      cosh = math.cosh, deg = math.deg, exp = math.exp, floor = math.floor, 
      fmod = math.fmod, frexp = math.frexp, huge = math.huge, 
      ldexp = math.ldexp, log = math.log, log10 = math.log10, max = math.max, 
      min = math.min, modf = math.modf, pi = math.pi, pow = math.pow, 
      rad = math.rad, random = math.random, sin = math.sin, sinh = math.sinh, 
      sqrt = math.sqrt, tan = math.tan, tanh = math.tanh },
  os = { clock = os.clock, difftime = os.difftime, time = os.time },
}

function run_sandbox(sb_env, sb_func, ...)
  local sb_orig_env=_ENV
  if (not sb_func) then return nil end
  _ENV=sb_env
  local sb_ret={e.pcall(sb_func, ...)}
  _ENV=sb_orig_env
  return e.table.unpack(sb_ret)
end

然后使用它,您将调用函数(my_func),如下所示:

pcall_rc, result_or_err_msg = run_sandbox(sandbox_env, my_func, arg1, arg2)

1
为什么不使用setfenv?我是Lua新手,所以很好奇它的区别在哪里。 - Lilith River
9
@计算机语言学家:setfenv 已经从 Lua 5.2 中移除:http://www.lua.org/work/doc/manual.html#8.2 - BMitch
1
我试图运行你的代码,但是我不明白你在哪里声明了my_func以及如何使其工作。它报错说:“42: attempt to index upvalue 'e' (a nil value)”。 - AlfredoVR
1
这个不起作用。函数使用它们编译时的_ENV,而不是它们被调用时的_ENV。在调用之前,您需要调用debug.setupvalue(sb_func,1,sb_env)来替换它的_ENV。 - John K
1
在5.1中,加载函数后交换_ENV是一种方法。在5.2中,您只需将sb_env作为ENV参数传递给“load”,即load(“function sb_func() return nil end”,“”,“t”,sb_env),然后每次都可以像常规函数一样调用sb_func。 - John K
显示剩余6条评论

14

5

清除不需要的内容最简单的方法之一是首先加载一个自己设计的Lua脚本,该脚本可以执行以下操作:

load = nil
loadfile = nil
dofile = nil

或者,您可以使用setfenv创建一个受限制的环境,并将特定的安全函数插入其中。

完全安全的沙箱有一些难度。如果从任何地方加载代码,请注意预编译代码可能会导致Lua崩溃。即使是完全受限制的代码,如果没有关闭它的系统,也可能进入无限循环并无限期地阻塞。


你实际上不需要加载Lua脚本来将某些内容置空 - 你可以使用我在回答中提到的Lua API函数来从Lua外部将全局变量置空。 - Amber
确实,但在某些方面这更容易,因此才有“最容易”的资格。 - John Calsbeek

4
你可以使用Lua API提供的lua_setglobal函数将这些值设置为nil,从而在全局命名空间中有效地防止任何用户脚本能够访问它们。
lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "io");

lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "loadfile");

...etc...

7
从安全角度考虑,如果有白名单解决方案可用,我永远不会相信黑名单解决方案(因为我可能会忘记某些可以被滥用的功能)。 - David

1
如果您使用的是Lua 5.1,请尝试这样做:

blockedThings = {'os', 'debug', 'loadstring', 'loadfile', 'setfenv', 'getfenv'}
scriptName = "user_script.lua"

function InList(list, val) 
    for i=1, #list do if list[i] == val then 
        return true 
    end 
end

local f, msg = loadfile(scriptName)

local env = {}
local envMT = {}
local blockedStorageOverride = {}
envMT.__index = function(tab, key)
    if InList(blockedThings, key) then return blockedStorageOverride[key] end
    return rawget(tab, key) or getfenv(0)[key]
end
envMT.__newindex = function(tab, key, val)
    if InList(blockedThings, key) then
        blockedStorageOverride[key] = val
    else
        rawset(tab, key, val)
    end
end

if not f then
    print("ERROR: " .. msg)
else
    setfenv(f, env)
    local a, b = pcall(f)
    if not a then print("ERROR: " .. b) end
end

4
我无法对沙盒技术发表评论,但我建议将blockedThings看起来更像{os = true,debug = true}这样的集合,然后检查是否为blockedThings [key],您就不需要InList函数了。 - mlepage

-2

您可以覆盖(禁用)任何您想要的 Lua 函数,同时您也可以使用 元表 来获得更多的 控制


14
元表可以通过rawget()绕过,不应该用于安全,只能用于方便。 - Amber
你不能覆盖 rawget 吗? - RCIX
RCIX,你能行的。在Lua中,你有自由做几乎任何事情 :) - Nick Dandoulakis
1
可以覆盖rawget,但这会破坏非恶意元表功能,并不是理想的解决方案。 - Amber

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