用 Lua 5.4 构建安全、可验证的配置 DSL
Lua 很适合表达层级配置,但“配置文件是 Lua”也意味着它默认可以循环、分配、访问开放库并调用宿主暴露的任何能力。安全配置 DSL 的目标不是假装脚本不是代码,而是建立一个窄语言:只提供白名单构造函数,把执行结果转换成普通数据,再进行完整结构和语义验证。示例使用 Lua 5.4.8。
设计返回值合同
配置 chunk 应返回一个值,不修改全局变量。例如:
return service {
name = "catalog",
replicas = 3,
endpoints = {
endpoint { path = "/health", method = "GET" },
endpoint { path = "/items", method = "POST" }
}
}
service 和 endpoint 是宿主提供的纯构造函数。它们复制允许字段、附加类型标记并尽早拒绝未知键。最终结果仍要经过第二次验证,因为脚本可以直接返回自己构造的 table,不能把“使用了漂亮 DSL”当安全证明。
为每次加载创建新环境
load 可以接收环境表。只放入 DSL 构造器和必要的纯常量,不通过 __index = _G 回退到真实全局环境:
local function make_environment()
local env = {
service = build_service,
endpoint = build_endpoint,
true_value = true
}
return setmetatable(env, {
__newindex = function(_, key)
error("global assignment is not allowed: " .. tostring(key), 2)
end
})
end
local function parse(source, name)
local chunk, err = load(source, name, "t", make_environment())
if not chunk then return nil, err end
local ok, value = pcall(chunk)
if not ok then return nil, value end
return validate_and_normalize(value)
end
模式参数使用 "t" 只接受文本 chunk,避免加载预编译二进制。每次新环境防止一个配置向另一个泄漏变量。这里的 error、tostring 由宿主闭包捕获,不必把它们暴露给配置。
白名单函数必须无副作用
构造器不读取文件、环境变量、网络或当前时间,也不修改宿主全局状态。否则同一配置在不同机器会产生不同结果,审查文本也无法知道真实输入。需要环境差异时,在配置外部选择明确 profile,或把已经验证的常量作为只读值传入并记录来源。
不要暴露 os、io、package、debug、require、dofile 或 load。移除这些名字只是第一层;任何宿主 userdata 和函数都可能间接提供能力。审查整个可达对象图,并避免把带危险 metatable 的共享 table 放进环境。
验证结构和语义
验证器检查根类型、必需字段、未知字段、字符串长度、数值范围、数组是否连续以及嵌套深度。再检查跨字段规则,例如 endpoint 路径唯一、POST 不允许被标记只读、replicas 与环境上限一致。错误包含配置名和字段路径,不回显整份敏感内容。
规范化输出为宿主自己的不可变结构:填充默认值、统一枚举大小写、复制 table,丢弃 metatable 与函数。运行阶段只使用规范化结果,不长期持有脚本返回表。这样后续代码不需要继续防御 Lua 动态行为。
设置资源预算
即使环境没有 I/O,脚本仍可 while true do end 或构造巨大 table。嵌入宿主可以使用 allocator 限制内存,并通过 hook 检查指令预算或取消。在纯 Lua 进程中,可靠地隔离恶意代码更困难;高风险输入应放入受 OS 限制的独立进程。不要把“受控 _ENV”称为完整安全沙箱。
配置大小、解析时间、嵌套深度和产出条目数都应有上限。部署前离线验证,生产加载失败时保留上一份已知良好配置,不能把半解析结果投入运行。
测试与版本化 DSL
黄金测试把配置文本转换成规范化结构,错误测试覆盖未知全局、赋值、语法错误、无限循环预算、重复键和超限值。对 DSL 设显式版本;删除或改变字段时提供迁移错误,而不是静默改变含义。格式化工具和 JSON 导出能帮助审查最终结果。
结论
安全 Lua 配置的核心是能力最小化:新环境、白名单纯构造器、文本模式加载、受保护执行、资源预算和完整验证。脚本输出必须复制成宿主拥有的规范结构,失败则保留已知良好版本。Lua 提供的是可读表达能力;边界、安全和确定性仍由宿主负责。