在Lua 5.2中保护全局环境

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中有其记录。


你可能感兴趣的:(在Lua 5.2中保护全局环境)