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调用。

posted @ 2012-10-25 13:42 winter-cn 阅读(...) 评论(...) 编辑 收藏