所以 Lua 似乎是在我的应用程序中实现安全“用户脚本”的理想选择。
然而,大多数嵌入lua的例子似乎都包含加载所有标准库,包括“io”和“package”。
所以我可以从解释器中排除这些库,但即使是基础库也包含访问文件系统的函数“dofile”和“loadfile”。
如何删除/阻止任何此类不安全函数,而不会最终得到一个甚至没有“ipairs”函数等基本内容的解释器?
这是 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)
清除不良内容的最简单方法之一是首先加载您自己设计的 Lua 脚本,它可以执行以下操作:
load = nil
loadfile = nil
dofile = nil
或者,您可以使用
setfenv
创建一个可以在其中插入特定安全功能的受限环境。
完全安全的沙箱有点困难。如果您从任何地方加载代码,请注意预编译代码可能会导致 Lua 崩溃。如果您没有关闭系统,即使是完全受限的代码也可能会进入无限循环并无限期地阻塞。
您可以使用Lua API提供的
lua_setglobal
函数将全局命名空间中的这些值设置为nil
,这将有效阻止任何用户脚本访问它们。
lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "io");
lua_pushnil(state_pointer);
lua_setglobal(state_pointer, "loadfile");
...etc...
如果您使用 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
正如 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 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上运行时它们都传入了两种方法,但在本地尝试一下。)
您可以覆盖(禁用)任何您想要的 Lua 函数,也可以使用 metatables 进行更多控制。