为 lua 插件提供一个安全的环境
wow 开放了一套用户自定义的插件系统,很多人都认为,这套系统是 wow 成功的因素之一。反观国内乃至韩国的网游,至今没有一款游戏能提供相当自由度的用户自定义插件系统。
最开始,暴雪是想让用户可以由用户甚至第三方自定义操作界面。后来,这套基于 XML 和 lua 的插件系统不仅仅用来做界面了。
从我在网游行业从业这么多年的经验,游戏界面相关的开发也是颇费人力的。甚至于,Client 开发到了维护期,几乎都是在 UI 上堆砌人工。一套自由的插件系统,对于开发商和用户是双赢的。
但是,插件系统也是双面剑。最典型的负面问题就是安全。越是自由,越是给机器人制作开放了自由之门。这里暂且不提这个方面的问题。首先关注一下另一个:尽可能的保护系统中不想被插件系统访问的数据,避免利用插件编写木马。
lua 有第三方库提供了很好的解决方案。
比如可以考虑 Lua Rings ,通过它,可以提供多个 lua state 做 sandbox 。如果在 state 间有高交互性,那么或许有一些性能问题。
如果不使用第三方库,我们就回头看看语言本身,其实 lua 语言已经提供了很多机制来隔绝模块间的数据交换。
最常用的是 setfenv ,为函数设置环境。环境可以限制函数能访问的数据范围。但是,我们也需要警惕这方面的漏洞。比如你想给插件的环境提供一些基本 api ,例如 pairs print 等等。简单的把这些 api 复制到插件环境是不行的。
因为用户可以用 getfenv 取出这些 api 的环境,从而突破你的封锁。除非,你干脆不提供 getfenv 给用户使用。
正确的方法是用 metatable 做一个自动封装,当用户调用权限允许的 api 时,生成一个 closure 去调用这些 api ,并给 closure 加上受限的插件工作环境。
另一个问题是如何把内部对象暴露给插件系统。简单的把系统中的 lua 对象交给用户是不安全的。用户用一个 for 就可以迭代出所有的细节。
正确的做法是做一个 proxy ,设置元表来做权限检查,只让用户访问可以访问的数据。我的个人推荐是,先为对象实现一套属性机制(采用 index 和 newindex 元方法),仅把属性机制暴露给用户,而避免用户直接调用操作对象的方法。对属性做权限检查要简单的多。(我想,开发商一定不愿意让用户可以通过插件获得密码框里的输入数据吧?但是你的引擎却一定办的到)
剩下的问题还剩一个:如何避免用户通过 getmetatable 去获取元表?
阻止用户获取系统对象的元表的方法很简单:为你的元表设置 metatable 元方法。那么 getmetatable 就不会再返回真实的元表,而是返回元表中 metatable 项的东西。
而制作这样的 proxy 对象,你可以简单的用个空表,加上经保护的元表即可。注意:不要把真实对象放到这个 proxy 表内,而应该另外建一张插件环境访问不到的弱表做映射。
还有另一个选择是使用未公开的 api :newproxy 。不要被 unsupported and undocumented 的字样吓到。如果你有需要用它,即使以后官方取消了对其的支持,你也可以在 C 里自行实现一个。
在保护 metatable 方面,userdata 比 table 更安全。
有人问,为什么 newproxy 的行为这么怪异,为不同的(0 字节)userdata 赋予相同的 metatable ?其实看过上面的应用就应该明白了。我们需要的仅仅是对象的唯一性而已。即使不用 userdata 制作 proxy ,也是用张空的 table 而已。真实的对象不应该放在 proxy 里面,而是在外面放张弱表做索引(防止用户得到真实的对象)。使用 userdata 也一样,newproxy 返回的对象是 unique 的,正好做索引用。至于元表,一类对象公用一个就足够了。
最后,需要注意的是:如果你的内部机制需要传递内部对象。比如在我们的系统中,对象的组织关系以树结构管理。就可能出现下面这样的代码:
a.parent = b
把 a 挂在 b 的分支上,其中 parent 是 a 的一个属性:最终,engine 调用了 api :
a:setParent(b)
其中,b 就是 link 方法的参数,类型是我们自定义的对象。
把这些放到插件环境中,a 和 b 都不可能直接暴露给用户,暴露的只能是 a 和 b 的 proxy 。
proxya.parent = proxyb
最终会被转义为:
a:setParent(proxyb) 而不是 a:setParent(b)
这里的问题根源是,lua 的元表做不到 C++ 里那种对象类型的隐式转换。(即不能象 C++ 里那样,为 proxyObject 定义一个 operator Object() 的操作)
解决方法有二:
其一:属性赋值的时候,检查右值是不是 proxy 对象,若是,则转换成真实对象。这个方法直接,但是需要在底层暴露 proxy 这个概念。
其二:定义一个 plug 的内部属性,返回真实对象。并在权限控制上,禁止插件环境访问这个内部属性。即把
proxya.parent = proxyb
转义为:
a:setParent(b:getPlug())
Comments
Posted by: imagebody | (3) April 19, 2009 10:07 PM
Posted by: 王药师 | (2) April 7, 2009 08:50 PM
Posted by: Anonymous | (1) April 6, 2009 10:59 PM