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" }
  }
}

serviceendpoint 是宿主提供的纯构造函数。它们复制允许字段、附加类型标记并尽早拒绝未知键。最终结果仍要经过第二次验证,因为脚本可以直接返回自己构造的 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,避免加载预编译二进制。每次新环境防止一个配置向另一个泄漏变量。这里的 errortostring 由宿主闭包捕获,不必把它们暴露给配置。

白名单函数必须无副作用

构造器不读取文件、环境变量、网络或当前时间,也不修改宿主全局状态。否则同一配置在不同机器会产生不同结果,审查文本也无法知道真实输入。需要环境差异时,在配置外部选择明确 profile,或把已经验证的常量作为只读值传入并记录来源。

不要暴露 osiopackagedebugrequiredofileload。移除这些名字只是第一层;任何宿主 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 提供的是可读表达能力;边界、安全和确定性仍由宿主负责。