Lua脚本语言十分强大,但是有一个问题就是全局可写,比如你定义一个全局变量很容易不小心被另一个同名变量给覆盖掉。 这种问题一旦出现是十分难以调查的,该文章介绍的这种机制可以解决该问题。
我已经在我自己的工程中应用了该技术,它可以达到以下目的: 1.全局变量不能直接在Lua中被修改 2.可以创建出不能直接被修改的table 3.屏蔽一些你不想开放的Lua原生函数比如文件操作
注:我是混合着使用C和Lua实现该机制的,但是在纯Lua里也可以同样实现。为了便于表述,我这里只给出纯Lua版的例子。 另外该范例代码仅限于Lua 5.2版,但是该技巧同样可以适用于其他版本,但可能需要修改该一部分代码。
首先将所有安全机制的代码放进一个Lua脚本文件safe.lua如下: - -- 仅支持Lua 5.2版
- assert(_VERSION == "Lua 5.2")
- -- 全局环境在注册表中的索引值(见lua.h)
- local LUA_RIDX_GLOBALS = 2
- -- 安全table的metatable标志
- local SAFE_TABLE_FLAG = ".SAFETABLE"
- -- 设置全局安全保护机制
- local function SetupGlobal()
- -- 获取注册表
- local reg = debug.getregistry()
- local env = {} -- 新环境table
- local proxy = {} -- 代理table
- local mt = {} -- metatable
- -- 操作重载
- mt.__index = proxy
- mt.__newindex = function() print("cannot modify global enviroment!") end
- mt.__len = function() return #proxy end
- mt.__pairs = function() return pairs(proxy) end
- mt.__ipairs = function() return ipairs(proxy) end
- -- 隐藏metatable
- mt.__metatable = 0
- -- 标记为安全table
- mt[SAFE_TABLE_FLAG] = true
- -- 获取旧环境
- local old_env = reg[LUA_RIDX_GLOBALS]
- -- 设置新环境的metatable
- setmetatable(env, mt)
- -- 启用新环境
- _ENV = env
- -- 将全局默认环境也改为新环境
- reg[LUA_RIDX_GLOBALS] = env
- -- 返回代理table和旧环境
- return proxy, old_env
- end
- -- 新建一个有安全保护的table
- local function CreateSafeTable(base)
- local new = {} -- 新table
- local mt = {} -- metatable
- -- 如果没有指定base则新建一个空table
- local proxy = (type(base) == "table") and base or {}
- -- 操作重载
- mt.__index = proxy
- mt.__newindex = function() print("cannot modify safe table!") end
- mt.__len = function() return #proxy end
- mt.__pairs = function() return pairs(proxy) end
- mt.__ipairs = function() return ipairs(proxy) end
- -- 隐藏metatable
- mt.__metatable = 0
- -- 标记为安全table
- mt[SAFE_TABLE_FLAG] = true
- -- 设置新table的metatable
- setmetatable(new, mt)
- -- 返回新table和对应的代理table
- return new, proxy
- end
- -- 开启全局保护
- local proxy, old_env = SetupGlobal()
- -- 在这里复制需要导出给新环境使用的Lua原生全局变量和函数
- -- 被屏蔽的原生全局变量和函数有:
- -- _G Lua 5.2推荐使用_ENV(你可以根据需要把它定义为_ENV)
- -- dofile 我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
- -- loadfile 我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
- -- rawequal 需要覆盖,不应该直接操作安全table
- -- rawget 需要覆盖,不应该直接操作安全table
- -- rawlen 需要覆盖,不应该直接操作安全table
- -- rawset 需要覆盖,不应该直接操作安全table
- -- require 我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
- proxy._VERSION = old_env._VERSION
- proxy.assert = old_env.assert
- proxy.collectgarbage = old_env.collectgarbage
- proxy.error = old_env.error
- proxy.getmetatable = old_env.getmetatable
- proxy.ipairs = old_env.ipairs
- proxy.load = old_env.load
- proxy.next = old_env.next
- proxy.pairs = old_env.pairs
- proxy.pcall = old_env.pcall
- proxy.print = old_env.print
- proxy.select = old_env.select
- proxy.setmetatable = old_env.setmetatable
- proxy.tostring = old_env.tostring
- proxy.tonumber = old_env.tonumber
- proxy.type = old_env.type
- proxy.xpcall = old_env.xpcall
- -- 在这里导出给新环境使用的Lua原生全局table(将被设为只读table)
- -- 被屏蔽的原生全局table有:
- -- coroutine 我的工程里不需要coroutine,我没有评估过开放它对安全性有没有影响
- -- debug 会严重影响安全性,必须屏蔽
- -- io 我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
- -- os 我的工程里不需要os,我没有评估过开放它对安全性有没有影响
- -- package 我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
- proxy.bit32 = CreateSafeTable(old_env.bit32)
- proxy.math = CreateSafeTable(old_env.math)
- proxy.string = CreateSafeTable(old_env.string)
- proxy.table = CreateSafeTable(old_env.table)
- -- 实现安全版的rawequal
- proxy.rawequal = function(v1, v2)
- -- 获得真实的metatable
- local mt1 = old_env.debug.getmetatable(v1)
- local mt2 = old_env.debug.getmetatable(v2)
- -- 如果是安全table则使用代理table
- if mt1 and mt1[SAFE_TABLE_FLAG] then
- v1 = mt1.__index
- end
- if mt2 and mt2[SAFE_TABLE_FLAG] then
- v2 = mt2.__index
- end
- -- 调用原始rawequal
- return old_env.rawequal(v1, v2)
- end
- -- 实现安全版的rawget
- proxy.rawget = function(t, k)
- -- 获得真实的metatable
- local mt = old_env.debug.getmetatable(t)
- -- 如果是安全table则使用代理table
- if mt and mt[SAFE_TABLE_FLAG] then
- t = mt.__index
- end
- -- 调用原始rawget
- return old_env.rawget(t, k)
- end
- -- 实现安全版的rawlen
- proxy.rawlen = function(v)
- -- 获得真实的metatable
- local mt = old_env.debug.getmetatable(v)
- -- 如果是安全table则使用代理table
- if mt and mt[SAFE_TABLE_FLAG] then
- v = mt.__index
- end
- -- 调用原始rawlen
- return old_env.rawlen(v)
- end
- -- 实现安全版的rawset
- proxy.rawset = function(t, k, v)
- -- 获得真实的metatable
- local mt = old_env.debug.getmetatable(t)
- -- 如果是安全table则使用代理table
- if mt and mt[SAFE_TABLE_FLAG] then
- t = mt.__index
- end
- -- 调用原始rawset
- return old_env.rawset(t, k, v)
- end
- -- 这里可以自定义一些自己的内容
- -- 脚本文件装载列表
- local loaded_proxy
- proxy.LOADED, loaded_proxy = CreateSafeTable()
- -- 导入脚本文件
- proxy.import = function(s)
- -- 如果已经被导入则返回true
- if LOADED[s] ~= nil then
- return true
- end
- -- 装载文件
- local f, msg = old_env.loadfile(s)
- -- 如果装载失败,输出错误
- if not f then
- old_env.io.stderr:write(msg)
- return false
- end
- -- 否则执行该脚本
- local r, msg = pcall(f)
- -- 如果执行过程中出错,输出错误
- if not r then
- old_env.io.stderr:write(msg)
- return false
- end
- -- 记录文件名到装载列表
- loaded_proxy[s] = f
- -- 成功
- return true
- end
- -- 由于外界(这里指的是main.lua)环境已经初始化过环境了,没办法在safe.lua里直接更改(我没找到办法)
- -- 因此这里返回新环境给main.lua,main.lua需要在装载完该文件后把自己的环境设为该新环境
- -- 对于C这一步是不需要的,本身main.lua做作的一切可以都在C里完成
- do return _ENV end
-
复制代码 入口脚本main.lua:- -- 开启全局保护,并且更新自己的环境(见safe.lua末尾的说明)
- _ENV = dofile("safe.lua")
- -- 装载其他脚本
- import("test.lua")
- -- 输出已装载脚本
- for k, v in pairs(LOADED) do
- print("["..k.."] = "..tostring(v))
- end
- -- 尝试重复装载脚本
- import("test.lua")
-
复制代码 测试脚本test.lua:- -- 尝试定义全局变量
- x = 1
- print(x)
- -- 尝试修改已有全局变量
- print = nil
- print(print)
- -- 尝试修改安全table
- math.x = 0
- print(math.x)
- math.sin = nil
- print(math.sin)
-
复制代码 命令行里敲入lua main.lua,执行结果将为:- cannot modify global enviroment!
- nil
- cannot modify global enviroment!
- function: 6D793C3C
- cannot modify safe table!
- nil
- cannot modify safe table!
- function: 6D796C34
- [test.lua] = function: 003E8310
复制代码 可以看出所有写操作都没有成功,并且test.lua只加载了一次,在LOADED中有其记录.
|