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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM