Lua 环境基础
在 Lua 中,环境(environment)是一个表,所有全局变量的读写都通过这个环境表进行。Lua 5.1 使用 setfenv 和 getfenv 来操作函数环境,Lua 5.2+ 改为 _ENV 上值机制。
-- Lua 5.1 风格
print(_G) -- 全局环境表
-- 所有全局变量都是 _G 表的字段
x = 10
print(_G.x) -- 10
print(_G["x"]) -- 10
-- _G 自身也在 _G 中
print(_G._G == _G) -- true
Lua 5.2+ 的 _ENV 机制
Lua 5.2 引入了 _ENV 变量替代了 setfenv/getfenv。每个代码块都被编译器隐式地包装在一个 _ENV 上值中:
-- 以下两段代码在编译器看来是等价的
function hello()
print("hello")
end
-- 编译后等价于
function hello()
_ENV.print("hello")
end
这意味着全局变量查找实际上是在 _ENV 表中查找,局部变量则不需要经过 _ENV。
-- 自定义环境
local env = {
print = print,
x = 42,
y = 100
}
-- load 函数接受环境参数(Lua 5.2+)
local code = [[
print("x = " .. x)
print("y = " .. y)
]]
local chunk = load(code, "test", "t", env)
chunk()
-- 输出:
-- x = 42
-- y = 100
loadstring / load 函数
load 函数可以动态编译和执行 Lua 代码字符串:
-- 基本用法
local chunk = load("return 1 + 2")
print(chunk()) -- 3
-- 带环境参数
local env = {a = 10, b = 20}
local chunk = load("return a + b", "calc", "t", env)
print(chunk()) -- 30
-- 安全执行
local ok, result = pcall(load("return 1/0"))
print(ok, result) -- true inf
load 的参数说明:
| 参数 | 说明 |
|---|---|
chunk | 代码字符串或返回代码片段的函数 |
chunkname | 用于错误信息的名称 |
mode | "t" 仅文本,"b" 仅字节码,"bt" 两者皆可 |
env | 执行环境表 |
构建安全沙箱
沙箱允许安全地执行不受信任的代码,限制其可以访问的 API 和资源:
基础沙箱实现
local function create_sandbox(allowed_api)
-- 创建受限环境
local env = {}
-- 白名单 API
for _, name in ipairs(allowed_api or {}) do
env[name] = _G[name]
end
-- 安全的数学函数
env.math = {
abs = math.abs, ceil = math.ceil, floor = math.floor,
max = math.max, min = math.min, sqrt = math.sqrt,
sin = math.sin, cos = math.cos, pi = math.pi,
random = math.random, huge = math.huge
}
-- 安全的字符串函数
env.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
}
-- 安全的表函数
env.table = {
insert = table.insert, remove = table.remove,
sort = table.sort, concat = table.concat,
unpack = table.unpack or unpack
}
-- 基本函数
env.print = print
env.pairs = pairs
env.ipairs = ipairs
env.tonumber = tonumber
env.tostring = tostring
env.type = type
env.select = select
env.next = next
env.pcall = pcall
env.xpcall = xpcall
env.error = error
env.assert = assert
env.unpack = table.unpack or unpack
-- 设置 _G 指向自身
env._G = env
return env
end
-- 使用沙箱执行代码
local function sandbox_run(code, env)
local chunk, err = load(code, "sandbox", "t", env)
if not chunk then
return false, "编译错误: " .. err
end
local ok, result = pcall(chunk)
if not ok then
return false, "运行错误: " .. tostring(result)
end
return true, result
end
-- 测试
local env = create_sandbox({"print"})
local ok, result = sandbox_run([[
local sum = 0
for i = 1, 100 do sum = sum + i end
return sum
]], env)
print(ok, result) -- true 5050
限制危险操作
-- 在沙箱环境中禁止危险操作
local function secure_sandbox()
local env = create_sandbox()
-- 禁止访问 io 和 os
env.io = nil
env.os = nil
-- 禁止 require
env.require = nil
-- 禁止 loadstring/load
env.load = nil
env.loadstring = nil
env.dofile = nil
env.loadfile = nil
-- 禁止 debug 库
env.debug = nil
-- 禁止 rawget/rawset
env.rawget = nil
env.rawset = nil
env.rawequal = nil
-- 禁止 collectgarbage
env.collectgarbage = nil
-- 添加受控的输出函数
local output = {}
env.print = function(...)
local args = {...}
local parts = {}
for i, v in ipairs(args) do
parts[i] = tostring(v)
end
table.insert(output, table.concat(parts, "\t"))
end
env.get_output = function()
return output
end
return env
end
执行超时控制
使用 debug.sethook 可以限制代码执行时间或步数:
local function sandbox_run_with_timeout(code, env, max_steps)
max_steps = max_steps or 1000000
local chunk, err = load(code, "sandbox", "t", env)
if not chunk then
return false, "编译错误: " .. err
end
local steps = 0
debug.sethook(function()
steps = steps + 1
if steps > max_steps then
error("执行超时:超过最大步数限制", 2)
end
end, "", 1000) -- 每 1000 条指令检查一次
local ok, result = pcall(chunk)
debug.sethook() -- 清除 hook
if not ok then
if string.find(tostring(result), "超时") then
return false, "执行超时"
end
return false, "运行错误: " .. tostring(result)
end
return true, result
end
-- 测试无限循环检测
local env = create_sandbox()
local ok, err = sandbox_run_with_timeout([[
while true do end
]], env, 10000)
print(ok, err) -- false 执行超时
内存使用限制
local function sandbox_run_with_memory_limit(code, env, max_mem_kb)
max_mem_kb = max_mem_kb or 1024 -- 默认 1MB
local chunk, err = load(code, "sandbox", "t", env)
if not chunk then
return false, "编译错误: " .. err
end
local start_mem = collectgarbage("count")
-- 设置分配器 hook(Lua 5.4 不支持直接 hook,需要 C 层控制)
-- 替代方案:定期检查
local function check_memory()
local current_mem = collectgarbage("count") - start_mem
if current_mem > max_mem_kb then
error(string.format("内存超限:%.1f KB > %d KB",
current_mem, max_mem_kb), 2)
end
end
-- 包装表创建操作
local original_newproxy = env.newproxy
env.check_memory = check_memory
local ok, result = pcall(chunk)
if not ok then
if string.find(tostring(result), "内存超限") then
return false, "内存超限"
end
return false, "运行错误: " .. tostring(result)
end
local used_mem = collectgarbage("count") - start_mem
return true, result, used_mem
end
插件系统实现
利用沙箱构建安全的插件系统:
local PluginSystem = {}
PluginSystem.__index = PluginSystem
function PluginSystem.new()
return setmetatable({
plugins = {},
hooks = {}
}, PluginSystem)
end
function PluginSystem:load_plugin(name, code)
-- 为每个插件创建独立环境
local env = create_sandbox()
-- 注入插件 API
env.register_hook = function(event, handler)
if not self.hooks[event] then
self.hooks[event] = {}
end
table.insert(self.hooks[event], {
plugin = name,
handler = handler
})
end
env.log = function(msg)
print(string.format("[%s] %s", name, msg))
end
local chunk, err = load(code, name, "t", env)
if not chunk then
return false, err
end
local ok, err = pcall(chunk)
if not ok then
return false, err
end
self.plugins[name] = env
return true
end
function PluginSystem:trigger(event, ...)
local hooks = self.hooks[event] or {}
for _, hook in ipairs(hooks) do
local ok, err = pcall(hook.handler, ...)
if not ok then
print(string.format("插件 %s 处理 %s 时出错: %s",
hook.plugin, event, err))
end
end
end
-- 使用示例
local system = PluginSystem.new()
system:load_plugin("logger", [[
register_hook("user_login", function(username)
log("用户登录: " .. username)
end)
register_hook("user_logout", function(username)
log("用户登出: " .. username)
end)
]])
system:load_plugin("stats", [[
local login_count = 0
register_hook("user_login", function(username)
login_count = login_count + 1
log(string.format("累计登录: %d 次", login_count))
end)
]])
system:trigger("user_login", "张三")
-- [logger] 用户登录: 张三
-- [stats] 累计登录: 1 次
多租户配置系统
local function create_config_env(base_config)
local env = setmetatable({}, {
__index = function(t, key)
return base_config[key]
end,
__newindex = function(t, key, value)
error("配置为只读,不能修改: " .. key)
end
})
-- 添加安全的辅助函数
env.get = function(key, default)
local val = base_config[key]
if val ~= nil then return val end
return default
end
env.has = function(key)
return base_config[key] ~= nil
end
return env
end
-- 配置文件在受限环境中求值
local config_env = create_config_env({
host = "localhost",
port = 8080,
debug = true
})
local config_code = [[
local url = string.format("http://%s:%d",
get("host", "127.0.0.1"),
get("port", 3000))
return {
server_url = url,
is_debug = get("debug", false)
}
]]
local chunk = load(config_code, "config", "t", config_env)
local config = chunk()
print(config.server_url) -- http://localhost:8080
print(config.is_debug) -- true
注意事项
构建沙箱时需要注意以下安全要点:
debug库是沙箱安全的最大威胁,必须完全禁止- 防止通过元方法(如
__index)间接访问被禁 API string.dump可以暴露函数内部信息,应该禁止_G引用必须指向沙箱环境而非真正的全局表package.loaded和require需要完全控制或禁止- LuaJIT 的 FFI 可以绕过所有 Lua 层安全限制,必须在沙箱中禁用
- 定期更新 Lua 版本以修补已知的沙箱逃逸漏洞
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。