九鼎创展论坛中文版English
登录 | 立即注册 设为首页收藏本站 切换到宽版
查看: 6529|回复: 2

在Lua 5.2中保护全局环境

[复制链接]
发表于 2013-2-27 20:33:13 | 显示全部楼层 |阅读模式

Lua脚本语言十分强大,但是有一个问题就是全局可写,比如你定义一个全局变量很容易不小心被另一个同名变量给覆盖掉。

这种问题一旦出现是十分难以调查的,该文章介绍的这种机制可以解决该问题。


我已经在我自己的工程中应用了该技术,它可以达到以下目的:

1.全局变量不能直接在Lua中被修改

2.可以创建出不能直接被修改的table

3.屏蔽一些你不想开放的Lua原生函数比如文件操作



注:我是混合着使用C和Lua实现该机制的,但是在纯Lua里也可以同样实现。为了便于表述,我这里只给出纯Lua版的例子。

另外该范例代码仅限于Lua 5.2版,但是该技巧同样可以适用于其他版本,但可能需要修改该一部分代码。


首先将所有安全机制的代码放进一个Lua脚本文件safe.lua如下:

  1. -- 仅支持Lua 5.2版
  2. assert(_VERSION == "Lua 5.2")

  3. -- 全局环境在注册表中的索引值(见lua.h)
  4. local LUA_RIDX_GLOBALS = 2

  5. -- 安全table的metatable标志
  6. local SAFE_TABLE_FLAG = ".SAFETABLE"

  7. -- 设置全局安全保护机制
  8. local function SetupGlobal()

  9.         -- 获取注册表
  10.         local reg = debug.getregistry()

  11.         local env = {}                        -- 新环境table
  12.         local proxy = {}                -- 代理table
  13.         local mt = {}                        -- metatable

  14.         -- 操作重载
  15.         mt.__index = proxy
  16.         mt.__newindex = function() print("cannot modify global enviroment!") end
  17.         mt.__len = function() return #proxy end
  18.         mt.__pairs = function() return pairs(proxy) end
  19.         mt.__ipairs = function() return ipairs(proxy) end

  20.         -- 隐藏metatable
  21.         mt.__metatable = 0

  22.         -- 标记为安全table
  23.         mt[SAFE_TABLE_FLAG] = true

  24.         -- 获取旧环境
  25.         local old_env = reg[LUA_RIDX_GLOBALS]

  26.         -- 设置新环境的metatable
  27.         setmetatable(env, mt)

  28.         -- 启用新环境
  29.         _ENV = env

  30.         -- 将全局默认环境也改为新环境
  31.         reg[LUA_RIDX_GLOBALS] = env

  32.         -- 返回代理table和旧环境
  33.         return proxy, old_env

  34. end

  35. -- 新建一个有安全保护的table
  36. local function CreateSafeTable(base)

  37.         local new = {}                        -- 新table
  38.         local mt = {}                        -- metatable

  39.         -- 如果没有指定base则新建一个空table
  40.         local proxy = (type(base) == "table") and base or {}

  41.         -- 操作重载
  42.         mt.__index = proxy
  43.         mt.__newindex = function() print("cannot modify safe table!") end
  44.         mt.__len = function() return #proxy end
  45.         mt.__pairs = function() return pairs(proxy) end
  46.         mt.__ipairs = function() return ipairs(proxy) end

  47.         -- 隐藏metatable
  48.         mt.__metatable = 0

  49.         -- 标记为安全table
  50.         mt[SAFE_TABLE_FLAG] = true

  51.         -- 设置新table的metatable
  52.         setmetatable(new, mt)

  53.         -- 返回新table和对应的代理table
  54.         return new, proxy

  55. end

  56. -- 开启全局保护
  57. local proxy, old_env = SetupGlobal()

  58. -- 在这里复制需要导出给新环境使用的Lua原生全局变量和函数
  59. -- 被屏蔽的原生全局变量和函数有:
  60. --        _G                        Lua 5.2推荐使用_ENV(你可以根据需要把它定义为_ENV)
  61. --        dofile                我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
  62. --        loadfile        我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
  63. --        rawequal        需要覆盖,不应该直接操作安全table
  64. --        rawget                需要覆盖,不应该直接操作安全table
  65. --        rawlen                需要覆盖,不应该直接操作安全table
  66. --        rawset                需要覆盖,不应该直接操作安全table
  67. --        require                我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
  68. proxy._VERSION = old_env._VERSION
  69. proxy.assert = old_env.assert
  70. proxy.collectgarbage = old_env.collectgarbage
  71. proxy.error = old_env.error
  72. proxy.getmetatable = old_env.getmetatable
  73. proxy.ipairs = old_env.ipairs
  74. proxy.load = old_env.load
  75. proxy.next = old_env.next
  76. proxy.pairs = old_env.pairs
  77. proxy.pcall = old_env.pcall
  78. proxy.print = old_env.print
  79. proxy.select = old_env.select
  80. proxy.setmetatable = old_env.setmetatable
  81. proxy.tostring = old_env.tostring
  82. proxy.tonumber = old_env.tonumber
  83. proxy.type = old_env.type
  84. proxy.xpcall = old_env.xpcall

  85. -- 在这里导出给新环境使用的Lua原生全局table(将被设为只读table)
  86. -- 被屏蔽的原生全局table有:
  87. --        coroutine        我的工程里不需要coroutine,我没有评估过开放它对安全性有没有影响
  88. --        debug                会严重影响安全性,必须屏蔽
  89. --        io                        我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
  90. --        os                        我的工程里不需要os,我没有评估过开放它对安全性有没有影响
  91. --        package                我的工程需要屏遮文件系统,我没有评估过开放它对安全性有没有影响
  92. proxy.bit32 = CreateSafeTable(old_env.bit32)
  93. proxy.math = CreateSafeTable(old_env.math)
  94. proxy.string = CreateSafeTable(old_env.string)
  95. proxy.table = CreateSafeTable(old_env.table)

  96. -- 实现安全版的rawequal
  97. proxy.rawequal = function(v1, v2)

  98.         -- 获得真实的metatable
  99.         local mt1 = old_env.debug.getmetatable(v1)
  100.         local mt2 = old_env.debug.getmetatable(v2)

  101.         -- 如果是安全table则使用代理table
  102.         if mt1 and mt1[SAFE_TABLE_FLAG] then
  103.                 v1 = mt1.__index
  104.         end
  105.         if mt2 and mt2[SAFE_TABLE_FLAG] then
  106.                 v2 = mt2.__index
  107.         end

  108.         -- 调用原始rawequal
  109.         return old_env.rawequal(v1, v2)

  110. end

  111. -- 实现安全版的rawget
  112. proxy.rawget = function(t, k)

  113.         -- 获得真实的metatable
  114.         local mt = old_env.debug.getmetatable(t)

  115.         -- 如果是安全table则使用代理table
  116.         if mt and mt[SAFE_TABLE_FLAG] then
  117.                 t = mt.__index
  118.         end

  119.         -- 调用原始rawget
  120.         return old_env.rawget(t, k)

  121. end

  122. -- 实现安全版的rawlen
  123. proxy.rawlen = function(v)

  124.         -- 获得真实的metatable
  125.         local mt = old_env.debug.getmetatable(v)

  126.         -- 如果是安全table则使用代理table
  127.         if mt and mt[SAFE_TABLE_FLAG] then
  128.                 v = mt.__index
  129.         end

  130.         -- 调用原始rawlen
  131.         return old_env.rawlen(v)

  132. end

  133. -- 实现安全版的rawset
  134. proxy.rawset = function(t, k, v)

  135.         -- 获得真实的metatable
  136.         local mt = old_env.debug.getmetatable(t)

  137.         -- 如果是安全table则使用代理table
  138.         if mt and mt[SAFE_TABLE_FLAG] then
  139.                 t = mt.__index
  140.         end

  141.         -- 调用原始rawset
  142.         return old_env.rawset(t, k, v)

  143. end

  144. -- 这里可以自定义一些自己的内容

  145. -- 脚本文件装载列表
  146. local loaded_proxy
  147. proxy.LOADED, loaded_proxy = CreateSafeTable()

  148. -- 导入脚本文件
  149. proxy.import = function(s)

  150.         -- 如果已经被导入则返回true
  151.         if LOADED[s] ~= nil then
  152.                 return true
  153.         end

  154.         -- 装载文件
  155.         local f, msg = old_env.loadfile(s)

  156.         -- 如果装载失败,输出错误
  157.         if not f then
  158.                 old_env.io.stderr:write(msg)
  159.                 return false
  160.         end

  161.         -- 否则执行该脚本
  162.         local r, msg = pcall(f)

  163.         -- 如果执行过程中出错,输出错误
  164.         if not r then
  165.                 old_env.io.stderr:write(msg)
  166.                 return false
  167.         end

  168.         -- 记录文件名到装载列表
  169.         loaded_proxy[s] = f

  170.         -- 成功
  171.         return true

  172. end

  173. -- 由于外界(这里指的是main.lua)环境已经初始化过环境了,没办法在safe.lua里直接更改(我没找到办法)
  174. -- 因此这里返回新环境给main.lua,main.lua需要在装载完该文件后把自己的环境设为该新环境
  175. -- 对于C这一步是不需要的,本身main.lua做作的一切可以都在C里完成
  176. do return _ENV end
复制代码
入口脚本main.lua:
  1. -- 开启全局保护,并且更新自己的环境(见safe.lua末尾的说明)
  2. _ENV = dofile("safe.lua")

  3. -- 装载其他脚本
  4. import("test.lua")

  5. -- 输出已装载脚本
  6. for k, v in pairs(LOADED) do
  7.         print("["..k.."] = "..tostring(v))
  8. end

  9. -- 尝试重复装载脚本
  10. import("test.lua")
复制代码
测试脚本test.lua:
  1. -- 尝试定义全局变量
  2. x = 1
  3. print(x)

  4. -- 尝试修改已有全局变量
  5. print = nil
  6. print(print)

  7. -- 尝试修改安全table
  8. math.x = 0
  9. print(math.x)
  10. math.sin = nil
  11. print(math.sin)
复制代码
命令行里敲入lua main.lua,执行结果将为:
  1. cannot modify global enviroment!
  2. nil
  3. cannot modify global enviroment!
  4. function: 6D793C3C
  5. cannot modify safe table!
  6. nil
  7. cannot modify safe table!
  8. function: 6D796C34
  9. [test.lua] = function: 003E8310
复制代码
可以看出所有写操作都没有成功,并且test.lua只加载了一次,在LOADED中有其记录.



回复

使用道具 举报

发表于 2013-2-28 14:34:54 | 显示全部楼层
真厉害,向你学习
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|手机版|小黑屋|深圳市九鼎创展科技官方论坛 ( 粤ICP备11028681号-2  

GMT+8, 2024-3-29 16:40 , Processed in 0.021983 second(s), 19 queries .

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表