如何创建安全的Lua沙箱?


74

因此,Lua似乎是在我的应用程序中实现安全的“用户脚本”的理想选择。

但是,大多数嵌入lua的示例似乎都包括加载所有标准库,包括“ io”和“ package”。

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

我如何才能删除/阻止任何此类不安全的函数,而又不以没有“ ipairs”函数之类的基本东西的解释器结尾呢?

Answers:


53

您可以通过setfenv()设置运行不受信任代码的功能环境。概述如下:

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

user_script功能只能访问其环境中的内容。因此,您可以显式添加希望不受信任的代码可以访问的功能(白名单)。在这种情况下,用户脚本只能访问ipairs,但没有别的(dofileloadfile,等)。

有关示例和Lua沙箱的更多信息,请参见Lua沙箱。


18
请注意,我相信应该是local env = {ipairs=ipairs}。而且,如果您要在交互式lua cli上运行此程序,请将整个程序包装成一个do ... end循环,这样就不会丢失本地变量。
BMitch 2011年

9
应当注意,这是Lua 5.1的处理方式。
约翰·K

在Lua 5.2及以上,你会使用load()env论点相反,这是一个更安全的替代setfenv()无论如何,因为你不能忘记它一样容易。
DarkWiiPlayer

34

这是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新手,所以很好奇两者之间的区别。
莉莉丝·里弗

9
@计算机语言学家:setfenv已从5.2中删除:lua.org/work/doc/manual.html#8.2
BMitch 2011年

1
我正在尝试运行您的代码,但我不知道您在哪里声明my_func以及如何进行这项工作。它发出“ 42:尝试索引升值'e'(无值)”的
叫声

1
这行不通。函数使用编译时使用的_ENV,而不使用调用它们的_ENV。您需要先调用debug.setupvalue(sb_func,1,sb_env)来替换它的_ENV。
John K

1
加载函数后交换_ENV是5.1方式。在5.2中,您只需将sb_env作为ENV参数传递给“ load”即可。load(“ function sb_func()return nil end”,“”,“ t”,sb_env)然后,您可以每次像常规函数一样调用sb_func。
约翰·K


5

清除不良情况的最简单方法之一是首先加载您自己设计的Lua脚本,该脚本执行以下操作:

load = nil
loadfile = nil
dofile = nil

或者,您可以setfenv用来创建一个受限的环境,可以在其中插入特定的安全功能。

完全安全的沙箱有点难。如果您从任何地方加载代码,请注意,预编译的代码可能会使Lua崩溃。如果没有关闭系统,甚至完全受限制的代码也可能陷入无限循环并无限期地阻塞。


实际上,您不必加载Lua脚本即可将所有内容清零-您可以使用我在回答中提到的Lua API函数从Lua外部清零全局变量。
琥珀色

的确如此,但这在某种程度上更容易,因此是“最简单”的资格。
2009年

3

您可以使用lua_setglobalLua API提供的功能在全局名称空间中设置这些值,以nil有效地防止任何用户脚本访问它们。

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

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

...etc...

7
从安全的角度来看,至少在有可用的白名单解决方案(请参阅上面的一些答案)时,我永远不会相信黑名单的解决方案(我可能会忘记一些可误用的功能)。
大卫,

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()绕过元表,并且仅出于方便起见,不应将元表用于安全性。
琥珀色

您是否可以覆盖rawget?
RCIX

RCIX,您可以做到。您可以自由地在Lua做任何事情:-)
Nick Dandoulakis 09年

1
可以覆盖rawget,但这也会破坏非恶意的metatable功能,这不是理想的解决方案。
琥珀色
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.