首页 / 技术文章地图 / 正文

【野路子】脚本沙盒:安全执行第三方 Lua 代码

发布:2026-09-20 07:41 | 作者:996 技术组 | 4 阅读
完整课程入口:996 全套课程体系Lua 学习路径幂尔框架 mirs.cn

什么时候需要沙盒

让玩家的 Lua 脚本在你服务器上跑、加载策划写的活动脚本、执行热更补丁包——这些场景的共同点是:代码不完全可信,但业务又必须执行它。直接 load 后执行等于把整个引擎交给对方:os.execute、io 库、甚至一个死循环就能把进程拖死。沙盒的目标是"让不可信代码能干活,但只能在你画的圈里干活"。

第一层:环境白名单

Lua 5.1 用 setfenv、5.3+ 用 _ENV,原理相同——给不可信代码换一个阉割过的全局环境。做法是构造一个白名单表,只放你认为安全的函数,找不到的再透传到只读的真全局:

lua
local safe = {
    pairs = pairs, ipairs = ipairs,
    table = table, string = string,
    math = { floor = math.floor, abs = math.abs },
    print = release_print,
}
setmetatable(safe, { __index = function(_, k)
    error("沙盒禁止访问: " .. tostring(k), 2)
end })

local f = load(untrustedCode, "patch", "t", safe)   -- 5.4:第四参数即 _ENV
if f then f() end

白名单的取舍原则:字符串、数学库基本安全;io、os 只保留必要的封装(比如只允许写指定目录的日志);load、require、debug 一律禁——它们是逃逸沙盒的三条暗道(load 能再造环境,debug.getupvalue 能摸到外面的真全局)。

第二层:资源限额

环境白名单挡住"干坏事",限额挡住"意外干爆"。两个必备限额:执行时间——用 debug.sethook 设计数钩子,每 100 万条指令检查一次已用时间,超时就 error 掉整个脚本;内存配额——定时对比 collectgarbage("count"),增量超限同样终止。没有这两个限额,一个策划手误的死循环就能让整个 M2 停摆,比恶意代码更常见。

lua
local budget = 5e7   -- 指令预算
debug.sethook(function()
    budget = budget - 1e6
    if budget <= 0 then error("沙盒脚本执行超时", 2) end
end, "", 1e6)
-- ... 执行不可信代码 ...
debug.sethook()

第三层:结果校验与隔离

沙盒脚本的产出要当输入对待:它生成的表、注册的回调、请求的接口调用,全部过一遍业务校验再采纳。执行环境用临时 table 隔离,跑完整体丢弃,不在主环境里留任何痕迹。

工程上的经验是:沙盒能力越强,白名单越难维护。把"允许第三方脚本做的事"收敛成十几个明确的 API(发公告、改数值、查数据),比开放一个缩水的完整标准库安全得多——沙盒设计的最高境界,是让不可信代码根本接触不到危险能力,而不是拦截它。

沙盒内可调用的引擎口

callfunbynpc(actor, npcidx, delaytime, func, sParam)

调用其他NPC的lua函数(注意:如果开启多线程,会导致执行失败),沙盒要限制的第一类能力,默认不开放

参数类型说明
actorobject玩家对象(必填参数)
npcidxintNPC索引(NPC配置表中的ID),特殊npcid:QF=999999999,QM=999999996,LuaCond=999999995,LuaFunc=999999994(必填参数)
delaytimeint延迟时间ms,0立即执行(必填参数)
funcstring函数名(必填参数)
sParamstring参数(必填参数)
lua
callfunbynpc(self, 999999999, 1000, 'test', 'abc=1')

callscript(actor, filename, label)

调用TXT脚本命令,脚本文本命令同样属于高危能力

参数类型说明
actorobject玩家对象(必填参数)
filenamestring文件名(必填参数)
labelinteger标签(必填参数)

callscriptex(actor, scriptname, arr)

调用传奇脚本命令,同上级别,开放前必须有白名单

参数类型说明
actorobject玩家对象(必填参数)
scriptnamestring脚本接口(必填参数)
arrany参数1~参数10(必填参数)

加载受限模块

require(path)

加载文件,沙盒环境里的加载同样要拦,否则可从磁盘绕过限制

参数类型说明
pathany文件名两种加载接口起始路径不同
lua
ssrNetMsgCfg = require("Envir/QuestDiary/net/NetMsgCfg.lua")

跳转调用

gotolabel(actor,type,label,range)

调用触发,触发跳转是常见的越权入口

参数类型说明
actorobject玩家对象(必填参数)
typeinteger触发模式:0小组成员触发1行会成员触发2当前地图的人物触发3当前角色范围的人物触发8当前国家的人物触发
labelstring跳转后的接口(必填参数)
rangeinteger触发模式=3时,该参数为指定的范围大小

日志告警

release_print(msg)

打印消息到控制台,开发期观察沙盒行为

参数类型说明
msgany打印内容

logact(play, logAct, loginfo, nParam1, nParam2, nParam3, nParam4, nParam5)

自定义日志,线上记录被拦截的越权调用

参数类型说明
playobject玩家对象
logActinteger日志ID大于等于10000以上
loginfostring日志内容
nParam1integer整数型(可空)最大支持21亿
nParam2integer整数型(可空)最大支持21亿
nParam3integer整数型(可空)最大支持21亿
作者履历与出处
本文由 996 技术组基于 996 引擎官方知识库与浮生梦老师课程体系整理,讲解体系出自多年商业端开发生产一线。作者团队长期从事传奇类引擎 Lua 后端逻辑、客户端界面与版本交付,内容以官方知识库与真实项目为出处,按版本持续修订。
© 威海旷世互娱 · 返回文章地图 · 课程体系 · 幂尔框架