如何创建安全的Lua沙箱?

问题描述 投票:0回答:8

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

然而,大多数嵌入lua的例子似乎都包含加载所有标准库,包括“io”和“package”。

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

如何删除/阻止任何此类不安全函数,而不会最终得到一个甚至没有“ipairs”函数等基本内容的解释器?

lua sandbox
8个回答
55
投票

在Lua 5.1中,您可以通过

setfenv()
设置运行不受信任代码的函数环境。 概要如下:

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

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

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


38
投票

这是 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)

14
投票

Lua 现场演示 包含一个(专门的)沙箱。 来源是免费提供的。


5
投票

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

load = nil
loadfile = nil
dofile = nil

或者,您可以使用

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

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


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...

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

0
投票

将 Lua 代码沙箱化为字符串

正如 John K 指出的,如果您在 5.2+ 中以字符串形式加载 Lua 代码,您应该利用

load()
函数的附加参数:

load (chunk [, chunkname [, mode [, env]]])

字符串 mode 控制块可以是文本还是二进制(即预编译块)。它可能是字符串“b”(仅限二进制块)、“t”(仅限文本块)或“bt”(二进制和文本)。默认为“bt”。

运行恶意制作的字节码可能会使解释器崩溃。

所以你想通过

mode = "t"

无论如何,如果结果函数有任何上值,则其第一个上值将设置为 env 的值(如果给定了该参数),或者设置为全局环境的值。其他上值用 nil 初始化。所有上值都是新鲜的,也就是说,它们不与任何其他函数共享。

本质上,如果您传递

env
那么该函数将无法访问除
env
中的内容之外的任何外部作用域变量。因此,它将被沙箱化。

local untrusted_code = [[io.open("/bin/ls", "w") io.write(" ")]]
local limited_env = {
    -- Some basic environment example.
    ipairs = pairs,
    pairs = pairs,
    print = print,
}

local untrusted_fn, message = load(untrusted_code, "sandboxed", "t", limited_env)
print("Load error:", message)
print(untrusted_fn())

它将成功加载代码(因为它是有效的lua),但无法运行,因为它成功阻止了对

io
的访问:

Load error: nil
lua: [string "sandboxed"]:1: attempt to index a nil value (global 'io')
stack traceback:
    [string "sandboxed"]:1: in local 'untrusted_fn'
    C:\sandbox\test.lua:87: in main chunk
    [C]: in ?

总结一下,加载一串lua代码并在有限的环境下运行:

local function load_and_run_in_sandbox(untrusted_code, env, ...)
    local untrusted_fn, message = load(untrusted_code, "load_and_run_in_sandbox", "t", env)
    if not untrusted_fn then
        print(message)
        return
    end
    return pcall(untrusted_fn, ...)
end

对 Lua 函数进行沙箱处理

虽然 Lua 5.2+ 不包含 setfenv,但您可以使用

getupvalue
自行实现。 leafo 有指南,但简而言之:

-- Source: https://leafo.net/guides/setfenv-in-lua52-and-above.html
local function setfenv(fn, env)
    local i = 1
    while true do
        local name = debug.getupvalue(fn, i)
        if name == "_ENV" then
            debug.upvaluejoin(fn, i, (function()
                return env
            end), 1)
            break
        elseif not name then
            break
        end

        i = i + 1
    end

    return fn
end

function run_in_sandbox(env, fn, ...)
    setfenv(fn, env)
    return pcall(fn, ...)
end

在我的系统(Lua 5.3 和 5.4)上,此方法成功捕获加载的字符串,而 BMitch 的解决方案 则不然。

这是我的测试(在tio上运行时它们都传入了两种方法,但在本地尝试一下。)


-2
投票

您可以覆盖(禁用)任何您想要的 Lua 函数,也可以使用 metatables 进行更多控制

最新问题
© www.soinside.com 2019 - 2025. All rights reserved.