JavaScript沙箱的构想
问题
我的目标,非常简单,就是希望能够在我自己的系统中使用别人写的代码,但是这些代码可能会污染全局变量,甚至可能是恶意的,破坏性的。我要保证这些代码被正确执行,并且其影响范围完全受到控制,这就是我想要的沙箱。
根据我自己的思考以及和一些朋友的讨论,我认为我主要需要解决四点:
1.变量访问问题:第三方可以使用变量名访问到全局变量。
2.this问题:函数执行时的默认this值就是全局变量。
3.eval和Function问题:eval可以动态地生成代码,这些代码只有到运行时才能确定。
4.literal以及自动装箱问题:[] {}以及function可以构造出一些内置类的实例,这样通过constructor和__proto__等能访问到原生的全局对象。
限制
在这个问题中,我不希望引入过于重型的解决方案,比如,使用Narcissus之类的js引擎去执行整个代码是可行的,但是其性能极大地限制了代码的能力。还有,因为一些库和框架(如wind.js)依赖某些动态特性,将eval和Function禁止也是无法接受的,甚至直接eval必须能够访问到其调用的上下文,这样的特性也必须被保留。
方案
变量访问问题的解决
一些轻量级的工具(如我的JSinJS和Esprima,UglifyJS等)可以解析AST(Abstract Syntax Tree 抽象语法树),根据抽象语法树,可以找出所有未声明但是已经被赋值使用的变量。
例如,以下代码:
var a;
function my() {
var i = j;
j = 2;
}
通过AST,可以找到j 和a是被引用的全局变量。
这个问题唯一的例外是with,with中的某些变量可能并非全局:
with({s:1}) {
s = 2;
}
因为with中的内容在运行时才能确定,所以无法预判,这里只能按最糟糕的情况处理,认为使用了全局s。
找到了所有被引用的全局变量之后,只要用一个IFFE(Immediately Invoked Function Expression立即执行的函数表达式)把代码套起来,并且声明那些没有声明的变量,就可以把全局变量变成局部变量了:
void function(){
var j,k; //generated from AST
var a;
function my() {
var i = k;
j = 2;
}
}()
我们还需要暴露一些全局的方法给第三方代码使用,在IFFE外面加一个with
with(safe_global)
void function(){ //……
safe_global的实现就可以自由定义了,暴露一些想要暴露的东西。
this问题的解决
this问题比较麻烦,在不修改代码的情况下已知是没有解决办法的。this的值在运行时决定,在AST中没有办法知道哪些是安全的。于是我的想法是,对于所有this加一个check:例如
function f(){
return this;
}
将会被变成
function f() {
return _$wrap(this);
}
_$wrap函数将会检查this是不是全局对象,必要时将其替换成 safe_window。
因为_$wrap函数同样在运行时做检查,所以可以有效解决this问题。
eval和Function问题的解决
eval分为直接eval和间接eval,ES规范要求直接eval必须能保留调用时的上下文,因此实现safe_eval的方式肯定是不行了(参看《无法封装的函数:eval》)。所幸直接eval可以从AST中直接找出来,生成的代码必须仍然使用eval,我的方案是:
eval(……);
变成
eval(_$check(……));
_$check函数将会在运行时递归地做全文中所述的AST检查,并把结果返回,这样直接eval的问题就得以解决了。
间接eval和Function的问题类似,其代码都是在全局执行的,问题在于我们无法从AST中直接识别出来,所以还是需要运行时处理。我的方案是把safe_global中的eval变成safe_eval。
safe_global.eval = function safe_eval(){
return global.eval(_$check(……));
};
Function的情况跟间接eval差不多,不多说了。
这里还存在一个致命的问题,就是safe_global中的eval会阻止直接eval找到真正的eval函数。根据eval函数行为的定义:
一个 eval 函数的直接调用是表示为符合以下两个条件的 CallExpression:
解释执行 CallExpression 中的 MemberExpression 的结果是个 引用 ,这个引用拥有一个 环境记录项 作为其基值,并且这个引用的名称是 "eval"。
以这个 引用 作为参数调用 GetValue 抽象操作的结果是 15.1.2.1 定义的标准内置函数。
我们可以将eval(xxx)变成一个IFFE。
eval(……);
变成
(function() { var eval = _$unsafe_eval; return eval(_$check(……)); }());
这样就保存了上下文,这个IFFE也能像eval一样用在表达式中。
literal以及自动装箱问题的解决
这些同样发生在运行时,所以无法通过AST分析来解决,因为也不可能,于是我的解决方案是在一个iframe中执行这些代码。
唯一值得注意的是需要修改Function.prototype.constructor到safe_Function,以避免不安全的Function调用。