文章目錄
問題
我的目標,非常簡單,就是希望能夠在我自己的系統中使用別人寫的代碼,但是這些代碼可能會汙染全域變數,甚至可能是惡意的,破壞性的。我要保證這些代碼被正確執行,並且其影響範圍完全受到控制,這就是我想要的沙箱。
根據我自己的思考以及和一些朋友的討論,我認為我主要需要解決四點:
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調用。