一文徹底搞懂前端沙箱
什么是“沙箱”
也稱(chēng)作:“沙箱/沙盒/沙盤(pán)”。沙箱是一種安全機(jī)制,為運(yùn)行中的程序提供隔離環(huán)境。通常是作為一些來(lái)源不可信、具破壞力或無(wú)法判定程序意圖的程序提供實(shí)驗(yàn)之用。沙箱能夠安全的執(zhí)行不受信任的代碼,且不影響外部實(shí)際代碼影響的獨(dú)立環(huán)境。
有哪些動(dòng)態(tài)執(zhí)行腳本的場(chǎng)景?
在一些應(yīng)用中,我們希望給用戶提供插入自定義邏輯的能力,比如 Microsoft 的 Office 中的 VBA,比如一些游戲中的 lua 腳本,F(xiàn)ireFox 的「油猴腳本」,能夠讓用戶發(fā)在可控的范圍和權(quán)限內(nèi)發(fā)揮想象做一些好玩、有用的事情,擴(kuò)展了能力,滿足用戶的個(gè)性化需求。
大多數(shù)都是一些客戶端程序,在一些在線的系統(tǒng)和產(chǎn)品中也常常也有類(lèi)似的需求,事實(shí)上,在線的應(yīng)用中也有不少提供了自定義腳本的能力,比如 Google Docs 中的 Apps Script,它可以讓你使用 JavaScript 做一些非常有用的事情,比如運(yùn)行代碼來(lái)響應(yīng)文檔打開(kāi)事件或單元格更改事件,為公式制作自定義電子表格函數(shù)等等。
與運(yùn)行在「用戶電腦中」的客戶端應(yīng)用不同,用戶的自定義腳本通常只能影響用戶自已,而對(duì)于在線的應(yīng)用或服務(wù)來(lái)講,有一些情況就變得更為重要,比如「安全」,用戶的「自定義腳本」必須嚴(yán)格受到限制和隔離,即不能影響到宿主程序,也不能影響到其它用戶。
另外,有一些牽扯「模板化」的前端框架,如Vue.js、Venom.js等都會(huì)用到動(dòng)態(tài)代碼執(zhí)行。
JavaScript中的沙箱實(shí)現(xiàn)
零、幾個(gè)基礎(chǔ)知識(shí)
什么是constructor
?JavaScript中constructor屬性指向創(chuàng)建當(dāng)前對(duì)象的構(gòu)造函數(shù),該屬性是存在原型里的,且是不可靠的?JavaScript中constructor屬性[2]
function?test()?{}
const?obj?=?new?test();
console.log(obj.hasOwnProperty('constructor'));?//?false
console.log(obj.__proto__.hasOwnProperty('constructor'));?//?true
console.log(obj.__proto__?===?test.prototype);?//?true
console.log(test.prototype.hasOwnProperty('constructor'));?//?true
/**?constructor是不可靠的?*/
function?Foo()?{}
Foo.prototype?=?{};
const?foo?=?new?Foo();
console.log(foo.constructor?===?Object);??//?true,可以看出不是Foo了?constructor也是一種用于創(chuàng)建和初始化class[3]創(chuàng)建的對(duì)象的特殊方法?Class構(gòu)造方法[4]
幾個(gè)典型的constructor:
(async?function(){})().constructor?===?Promise
//?瀏覽器環(huán)境下
this.constructor.constructor?===?Function
window.constructor.constructor?===?Function
//?node環(huán)境下
this.constructor.constructor?===?Function
global.constructor.constructor?===?FunctionJS Proxy getPrototypeOf()
handler.getPrototypeOf()是一個(gè)代理方法,當(dāng)讀取代理對(duì)象的原型時(shí),該方法就會(huì)被調(diào)用。語(yǔ)法:
const?p?=?new?Proxy(obj,?{
??getPrototypeOf(target)?{?// target 被代理的目標(biāo)對(duì)象。
??...
??}
});當(dāng) getPrototypeOf 方法被調(diào)用時(shí),this 指向的是它所屬的處理器對(duì)象,getPrototypeOf 方法的返回值必須是一個(gè)對(duì)象或者 null。
在 JavaScript 中,有下面這五種操作(方法/屬性/運(yùn)算符)可以觸發(fā) JS 引擎讀取一個(gè)對(duì)象的原型,也就是可以觸發(fā) getPrototypeOf() 代理方法的運(yùn)行:
?Object.getPrototypeOf()[5]?Reflect.getPrototypeOf()[6]?proto[7]?Object.prototype.isPrototypeOf()[8]?instanceof[9]
如果遇到了下面兩種情況,JS 引擎會(huì)拋出?TypeError[10]?異常:
?getPrototypeOf() 方法返回的不是對(duì)象也不是 null。?目標(biāo)對(duì)象是不可擴(kuò)展的,且 getPrototypeOf() 方法返回的原型不是目標(biāo)對(duì)象本身的原型。
基本用法:
const?obj?=?{};
const?proto?=?{};
const?handler?=?{
????getPrototypeOf(target)?{
????????console.log(target?===?obj);???//?true
????????console.log(this?===?handler);?//?true
????????return?proto;
????}
};
var?p?=?new?Proxy(obj,?handler);?//?obj是被代理的對(duì)象,也就是handler.getPrototypeOf的target參數(shù)
console.log(Object.getPrototypeOf(p)?===?proto);????//?true
5 種觸發(fā) getPrototypeOf 代理方法的方式:
const?obj?=?{};
const?p?=?new?Proxy(obj,?{
????getPrototypeOf(target)?{
????????return?Array.prototype;
????}
});
console.log(
????Object.getPrototypeOf(p)?===?Array.prototype,??//?true
????Reflect.getPrototypeOf(p)?===?Array.prototype,?//?true
????p.__proto__?===?Array.prototype,???????????????//?true
????Array.prototype.isPrototypeOf(p),??????????????//?true
????p?instanceof?Array?????????????????????????????//?true
);
兩種異常的情況:
//?getPrototypeOf()?方法返回的不是對(duì)象也不是?null
const?obj?=?{};
const?p?=?new?Proxy(obj,?{
????getPrototypeOf(target)?{
????????return?"foo";
????}
});
Object.getPrototypeOf(p);?//?TypeError:?"foo"?is?not?an?object?or?null
//?目標(biāo)對(duì)象是不可擴(kuò)展的,且?getPrototypeOf()?方法返回的原型不是目標(biāo)對(duì)象本身的原型
const?obj?=?Object.preventExtensions({});?//?obj不可擴(kuò)展
const?p?=?new?Proxy(obj,?{
????getPrototypeOf(target)?{
????????return?{};
????}
});
Object.getPrototypeOf(p);?//?TypeError:?expected?same?prototype?value
//?如果對(duì)上面的代碼做如下的改造就沒(méi)問(wèn)題
const?obj?=?Object.preventExtensions({});?//?obj不可擴(kuò)展
const?p?=?new?Proxy(obj,?{
????getPrototypeOf(target)?{?//?target就是上面的obj
????????return?obj.__proto__;?//?返回的是目標(biāo)對(duì)象本身的原型
????}
});
Object.getPrototypeOf(p);?//?不報(bào)錯(cuò)一、跟瀏覽器宿主環(huán)境一致的沙箱實(shí)現(xiàn)
構(gòu)建閉包環(huán)境
我們知道在 JavaScript 中的作用域(scope)只有全局作用域(global scope)、函數(shù)作用域(function scope)以及從 ES6 開(kāi)始才有的塊級(jí)作用域(block scope)。如果要將一段代碼中的變量、函數(shù)等的定義隔離出來(lái),受限于 JavaScript 對(duì)作用域的控制,只能將這段代碼封裝到一個(gè) Function 中,通過(guò)使用 function scope 來(lái)達(dá)到作用域隔離的目的。也因?yàn)樾枰@種使用函數(shù)來(lái)達(dá)到作用域隔離的目的方式,于是就有 IIFE(立即調(diào)用函數(shù)表達(dá)式),這是一個(gè)被稱(chēng)為“自執(zhí)行匿名函數(shù)”的設(shè)計(jì)模式。
(function?foo(){
????const?a?=?1;
????console.log(a);
?})();//?無(wú)法從外部訪問(wèn)變量?
?
?console.log(a)?//?拋出錯(cuò)誤:"Uncaught ReferenceError: a is not defined"當(dāng)函數(shù)變成立即執(zhí)行的函數(shù)表達(dá)式時(shí),表達(dá)式中的變量不能從外部訪問(wèn),它擁有獨(dú)立的詞法作用域。不僅避免了外界訪問(wèn) IIFE 中的變量,而且又不會(huì)污染全局作用域,彌補(bǔ)了 JavaScript 在 scope 方面的缺陷。一般常見(jiàn)于寫(xiě)插件和類(lèi)庫(kù)時(shí),如 JQuery 當(dāng)中的沙箱模式
(function?(window)?{
????var?jQuery?=?function?(selector,?context)?{
????????return?new?jQuery.fn.init(selector,?context);
????}
????jQuery.fn?=?jQuery.prototype?=?function?()?{
????????//原型上的方法,即所有jQuery對(duì)象都可以共享的方法和屬性
????}
????jQuery.fn.init.prototype?=?jQuery.fn;
????window.jQeury?=?window.$?=?jQuery;?//如果需要在外界暴露一些屬性或者方法,可以將這些屬性和方法加到window全局對(duì)象上去
})(window);當(dāng)將 IIFE 分配給一個(gè)變量,不是存儲(chǔ) IIFE 本身,而是存儲(chǔ) IIFE 執(zhí)行后返回的結(jié)果。
const?result?=?(function?()?{
????const?name?=?"張三";
????return?name;
})();
console.log(result);?//?"張三"原生瀏覽器對(duì)象的模擬
模擬原生瀏覽器對(duì)象的目的是為了防止閉包環(huán)境,操作原生對(duì)象,篡改污染原生環(huán)境,完成模擬瀏覽器對(duì)象之前我們需要先關(guān)注幾個(gè)不常用的 API。
eval
eval 函數(shù)可將字符串轉(zhuǎn)換為代碼執(zhí)行,并返回一個(gè)或多個(gè)值:
const?b?=?eval("({name:'張三'})");
console.log(b.name);由于 eval 執(zhí)行的代碼可以訪問(wèn)閉包和全局范圍,因此就導(dǎo)致了代碼注入的安全問(wèn)題,因?yàn)榇a內(nèi)部可以沿著作用域鏈往上找,篡改全局變量,這是我們不希望的。
console.log(eval(?this.window?===?window?));?//?true?補(bǔ)充幾個(gè)點(diǎn):
?性能&安全問(wèn)題,一般不建議在實(shí)際業(yè)務(wù)代碼中引入eval?輔助異步編程框架的windjs大量采用eval的寫(xiě)法來(lái)輔助編程,引發(fā)爭(zhēng)議?專(zhuān)訪 Wind.js 作者老趙(上):緣由、思路及發(fā)展[11]?瀏覽器環(huán)境下,(0, eval)()比eval()的性能要好「目前已經(jīng)不是了」(0, eval)(‘this’)[12]
const?times?=?1000;
const?time1?=?'直接引用';
const?time2?=?'間接引用';
let?times1?=?times;
console.time(time1);
while(times1--)?{
????eval(`199?+?200`);
}
console.timeEnd(time1);
let?times2?=?times;
console.time(time2);
while(times2--)?{
????(0,?eval)(`199?+?200`);
}
console.timeEnd(time2);new Function
Function構(gòu)造函數(shù)創(chuàng)建一個(gè)新的 Function 對(duì)象。直接調(diào)用這個(gè)構(gòu)造函數(shù)可用于動(dòng)態(tài)創(chuàng)建函數(shù)。
new?Function?([arg1[,?arg2[,?...argN]],]?functionBody)?arg1, arg2, ... argN?被函數(shù)使用的參數(shù)的名稱(chēng)必須是合法命名的。參數(shù)名稱(chēng)是一個(gè)有效的 JavaScript 標(biāo)識(shí)符的字符串,或者一個(gè)用逗號(hào)分隔的有效字符串的列表,例如“×”,“theValue”,或“a,b”。
補(bǔ)充幾個(gè)點(diǎn):
?new Function()性能一般比eval要好,很多用到這塊的前端框架都是用new Function()實(shí)現(xiàn)的,比如:Vue.js?打開(kāi)瀏覽器控制臺(tái)后,new Function()的性能要慢一倍以上
functionBody
一個(gè)含有包括函數(shù)定義的 JavaScript 語(yǔ)句的字符串。
const?sum?=?new?Function('a',?'b',?'return?a?+?b');?
console.log(sum(1,?2));//3?同樣也會(huì)遇到和 eval 類(lèi)似的的安全問(wèn)題和相對(duì)較小的性能問(wèn)題。
let?a?=?1;
function?sandbox()?{
????let?a?=?2;
????return?new?Function('return?a;');?//?這里的?a?指向最上面全局作用域內(nèi)的?1
}
const?f?=?sandbox();
console.log(f());與 eval 不同的是 Function 創(chuàng)建的函數(shù)只能在全局作用域中運(yùn)行,它無(wú)法訪問(wèn)局部閉包變量,它們總是被創(chuàng)建于全局環(huán)境,因此在運(yùn)行時(shí)它們只能訪問(wèn)全局變量和自己的局部變量,不能訪問(wèn)它們被 Function 構(gòu)造器創(chuàng)建時(shí)所在的作用域的變量。new Function()是 eval()更好替代方案。它具有卓越的性能和安全性,但仍沒(méi)有解決訪問(wèn)全局的問(wèn)題。
with
with 是 JavaScript 中一個(gè)關(guān)鍵字,擴(kuò)展一個(gè)語(yǔ)句的作用域鏈。它允許半沙盒執(zhí)行。那什么叫半沙盒?語(yǔ)句將某個(gè)對(duì)象添加到作用域鏈的頂部,如果在沙盒中有某個(gè)未使用命名空間的變量,跟作用域鏈中的某個(gè)屬性同名,則這個(gè)變量將指向這個(gè)屬性值。如果沒(méi)有同名的屬性,則將拋出 ReferenceError。
//?嚴(yán)格模式下以下代碼運(yùn)行會(huì)有問(wèn)題
function?sandbox(o)?{
????with?(o){
????????//a=5;?
????????c=2;
????????d=3;
????????console.log(a,b,c,d);?//?0,1,2,3 //每個(gè)變量首先被認(rèn)為是一個(gè)局部變量,如果局部變量與 obj 對(duì)象的某個(gè)屬性同名,則這個(gè)局部變量會(huì)指向 obj 對(duì)象屬性。
????}
}
const?f?=?{
????a:0,
????b:1
}
sandbox(f);?
??????
console.log(f);
console.log(c,d);?//?2,3?c、d被泄露到window對(duì)象上究其原理,with在內(nèi)部使用in運(yùn)算符。對(duì)于塊內(nèi)的每個(gè)變量訪問(wèn),它都在沙盒條件下計(jì)算變量。如果條件是 true,它將從沙盒中檢索變量。否則,就在全局范圍內(nèi)查找變量。但是 with 語(yǔ)句使程序在查找變量值時(shí),都是先在指定的對(duì)象中查找。所以對(duì)于那些本來(lái)不是這個(gè)對(duì)象的屬性的變量,查找起來(lái)會(huì)很慢,對(duì)于有性能要求的程序不適合(JavaScript 引擎會(huì)在編譯階段進(jìn)行數(shù)項(xiàng)的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過(guò)程中快速找到標(biāo)識(shí)符)。with 也會(huì)導(dǎo)致數(shù)據(jù)泄漏(在非嚴(yán)格模式下,會(huì)自動(dòng)在全局作用域創(chuàng)建一個(gè)全局變量)
in 運(yùn)算符
in 運(yùn)算符能夠檢測(cè)左側(cè)操作數(shù)是否為右側(cè)操作數(shù)的成員。其中,左側(cè)操作數(shù)是一個(gè)字符串,或者可以轉(zhuǎn)換為字符串的表達(dá)式,右側(cè)操作數(shù)是一個(gè)對(duì)象或數(shù)組。
const?o?=?{??
????a?:?1,??
????b?:?function()?{}
};
console.log("a"?in?o);??//true
console.log("b"?in?o);??//true
console.log("c"?in?o);??//false
console.log("valueOf"?in?o);??//返回true,繼承Object的原型方法
console.log("constructor"?in?o);??//返回true,繼承Object的原型屬性with + new Function
配合 with 用法可以稍微限制沙盒作用域,先從當(dāng)前的 with 提供對(duì)象查找,但是如果查找不到依然還能從更上面的作用域獲取,污染或篡改全局環(huán)境。
function?sandbox?(src)?{
????src?=?'with?(sandbox)?{'?+?src?+?'}';
????return?new?Function('sandbox',?src);
}
const?str?=?`
????let?a?=?1;?
????window.name="張三";?
????console.log(a);?//?打印:1
`;
sandbox(str)({});
console.log(window.name);//'張三'可以看到,基于上面的方案都多多少少存在一些安全問(wèn)題:
?eval 是全局對(duì)象的一個(gè)函數(shù)屬性,執(zhí)行的代碼擁有著和應(yīng)用中其它正常代碼一樣的的權(quán)限,它能訪問(wèn)「執(zhí)行上下文」中的局部變量,也能訪問(wèn)所有「全局變量」,在這個(gè)場(chǎng)景下,它是一個(gè)非常危險(xiǎn)的函數(shù)?使用 Function 構(gòu)造器生成的函數(shù),并不會(huì)在創(chuàng)建它的上下文中創(chuàng)建閉包,一般在全局作用域中被創(chuàng)建。當(dāng)運(yùn)行函數(shù)的時(shí)候,只能訪問(wèn)自己的本地變量和全局變量,不能訪問(wèn) Function 構(gòu)造器被調(diào)用生成的上下文的作用域?with 一樣的問(wèn)題,它首先會(huì)在傳入的對(duì)象中查找對(duì)應(yīng)的變量,如果找不到就會(huì)往更上層的全局作用域去查找,所以也避免不了污染或篡改全局環(huán)境
那有沒(méi)有更安全一些的沙箱環(huán)境實(shí)現(xiàn)呢?
基于 Proxy 實(shí)現(xiàn)的沙箱(ProxySandbox)
ES6 Proxy 用于修改某些操作的默認(rèn)行為,等同于在語(yǔ)言層面做出修改,屬于一種“元編程”(meta programming)
function?evalute(code,sandbox)?{
??sandbox?=?sandbox?||?Object.create(null);
??const?fn?=?new?Function('sandbox',?`with(sandbox){return?(${code})}`);
??const?proxy?=?new?Proxy(sandbox,?{
????has(target,?key)?{
??????//?讓動(dòng)態(tài)執(zhí)行的代碼認(rèn)為屬性已存在
??????return?true;?
????}
??});
??return?fn(proxy);
}
evalute('1+2')?//?3
evalute('console.log(1)')?//?Cannot?read?property?'log'?of?undefined我們知道無(wú)論 eval 還是 function,執(zhí)行時(shí)都會(huì)把作用域一層一層向上查找,如果找不到會(huì)一直到 global,那么利用 Proxy 的原理就是,讓執(zhí)行了代碼在 sandobx 中找的到,以達(dá)到「防逃逸」的目的。
我們前面提到with在內(nèi)部使用in運(yùn)算符來(lái)計(jì)算變量,如果條件是 true,它將從沙盒中檢索變量。理想狀態(tài)下沒(méi)有問(wèn)題,但也總有些特例獨(dú)行的存在,比如 Symbol.unscopables。
Symbol 對(duì)象的 Symbol.unscopables 屬性,指向一個(gè)對(duì)象。該對(duì)象指定了使用 with 關(guān)鍵字時(shí),哪些屬性會(huì)被 with 環(huán)境排除。
Array.prototype[Symbol.unscopables]
//?{//???copyWithin:?true,//???entries:?true,//???fill:?true,//???find:?true,//???findIndex:?true,//???keys:?true//?}Object.keys(Array.prototype[Symbol.unscopables])
//?['copyWithin',?'entries',?'fill',?'find',?'findIndex',?'keys']上面代碼說(shuō)明,數(shù)組有 6 個(gè)屬性,會(huì)被 with 命令排除。

由此我們的代碼還需要修改如下:
function?sandbox(code)?{
????code?=?'with?(sandbox)?{'?+?code?+?'}'
????const?fn?=?new?Function('sandbox',?code)
????return?function?(sandbox)?{
????????const?sandboxProxy?=?new?Proxy(sandbox,?{
????????????has(target,?key)?{
????????????????return?true
????????????},
????????????get(target,?key)?{
????????????????if?(key?===?Symbol.unscopables)?return?undefined
????????????????return?target[key]
????????????}
????????})
????????return?fn(sandboxProxy)
????}
}
const?test?=?{
????a:?1,
????log(){
????????console.log('11111')
????}
}
const?code?=?'log();?console.log(a)'?//?1111,TypeError:?Cannot?read?property?'log'?of?undefinedsandbox(code)(test)Symbol.unscopables 定義對(duì)象的不可作用屬性。Unscopeable 屬性永遠(yuǎn)不會(huì)從 with 語(yǔ)句中的沙箱對(duì)象中檢索,而是直接從閉包或全局范圍中檢索。
快照沙箱(SnapshotSandbox)
快照沙箱實(shí)現(xiàn)來(lái)說(shuō)比較簡(jiǎn)單,主要用于不支持 Proxy 的低版本瀏覽器,原理是基于diff來(lái)實(shí)現(xiàn)的,在子應(yīng)用激活或者卸載時(shí)分別去通過(guò)快照的形式記錄或還原狀態(tài)來(lái)實(shí)現(xiàn)沙箱,snapshotSandbox 會(huì)污染全局 window。
我們看下?qiankun[13]?的 snapshotSandbox 的源碼,這里為了幫助理解做部分精簡(jiǎn)及注釋。
function?iter(obj,?callbackFn)?{
????for?(const?prop?in?obj)?{
????????if?(obj.hasOwnProperty(prop))?{
????????????callbackFn(prop);
????????}
????}
}
/**
?*?基于?diff?方式實(shí)現(xiàn)的沙箱,用于不支持?Proxy?的低版本瀏覽器
?*/
class?SnapshotSandbox?{
????constructor(name)?{
????????this.name?=?name;
????????this.proxy?=?window;
????????this.type?=?'Snapshot';
????????this.sandboxRunning?=?true;
????????this.windowSnapshot?=?{};
????????this.modifyPropsMap?=?{};
????????this.active();
????}
????//激活
????active()?{
????????//?記錄當(dāng)前快照
????????this.windowSnapshot?=?{};
????????iter(window,?(prop)?=>?{
????????????this.windowSnapshot[prop]?=?window[prop];
????????});
????????//?恢復(fù)之前的變更
????????Object.keys(this.modifyPropsMap).forEach((p)?=>?{
????????????window[p]?=?this.modifyPropsMap[p];
????????});
????????this.sandboxRunning?=?true;
????}
????//還原
????inactive()?{
????????this.modifyPropsMap?=?{};
????????iter(window,?(prop)?=>?{
????????????if?(window[prop]?!==?this.windowSnapshot[prop])?{
????????????????//?記錄變更,恢復(fù)環(huán)境
????????????????this.modifyPropsMap[prop]?=?window[prop];
??????????????
????????????????window[prop]?=?this.windowSnapshot[prop];
????????????}
????????});
????????this.sandboxRunning?=?false;
????}
}
let?sandbox?=?new?SnapshotSandbox();
//test
((window)?=>?{
????window.name?=?'張三'
????window.age?=?18
????console.log(window.name,?window.age)?//????張三,18
????sandbox.inactive()?//????還原
????console.log(window.name,?window.age)?//????undefined,undefined
????sandbox.active()?//????激活
????console.log(window.name,?window.age)?//????張三,18
})(sandbox.proxy);legacySandBox
qiankun 框架 singular 模式下的 proxy 沙箱實(shí)現(xiàn),為了便于理解,這里做了部分代碼的精簡(jiǎn)和注釋。
//legacySandBox
const?callableFnCacheMap?=?new?WeakMap();
function?isCallable(fn)?{
??if?(callableFnCacheMap.has(fn))?{
????return?true;
??}
??const?naughtySafari?=?typeof?document.all?===?'function'?&&?typeof?document.all?===?'undefined';
??const?callable?=?naughtySafari???typeof?fn?===?'function'?&&?typeof?fn?!==?'undefined'?:?typeof?fn?===
????'function';
??if?(callable)?{
????callableFnCacheMap.set(fn,?callable);
??}
??return?callable;
};
function?isPropConfigurable(target,?prop)?{
??const?descriptor?=?Object.getOwnPropertyDescriptor(target,?prop);
??return?descriptor???descriptor.configurable?:?true;
}
function?setWindowProp(prop,?value,?toDelete)?{
??if?(value?===?undefined?&&?toDelete)?{
????delete?window[prop];
??}?else?if?(isPropConfigurable(window,?prop)?&&?typeof?prop?!==?'symbol')?{
????Object.defineProperty(window,?prop,?{
??????writable:?true,
??????configurable:?true
????});
????window[prop]?=?value;
??}
}
function?getTargetValue(target,?value)?{
??/*
????僅綁定 isCallable && !isBoundedFunction && !isConstructable 的函數(shù)對(duì)象,如 window.console、window.atob 這類(lèi)。目前沒(méi)有完美的檢測(cè)方式,這里通過(guò) prototype 中是否還有可枚舉的拓展方法的方式來(lái)判斷
????@warning?這里不要隨意替換成別的判斷方式,因?yàn)榭赡苡|發(fā)一些?edge?case(比如在?lodash.isFunction?在?iframe?上下文中可能由于調(diào)用了?top?window?對(duì)象觸發(fā)的安全異常)
???*/
??if?(isCallable(value)?&&?!isBoundedFunction(value)?&&?!isConstructable(value))?{
????const?boundValue?=?Function.prototype.bind.call(value,?target);
????for?(const?key?in?value)?{
??????boundValue[key]?=?value[key];
????}
????if?(value.hasOwnProperty('prototype')?&&?!boundValue.hasOwnProperty('prototype'))?{
??????Object.defineProperty(boundValue,?'prototype',?{
????????value:?value.prototype,
????????enumerable:?false,
????????writable:?true
??????});
????}
????return?boundValue;
??}
??return?value;
}
/**
?*?基于?Proxy?實(shí)現(xiàn)的沙箱
?*/
class?SingularProxySandbox?{
??/**?沙箱期間新增的全局變量?*/
??addedPropsMapInSandbox?=?new?Map();
??/**?沙箱期間更新的全局變量?*/
??modifiedPropsOriginalValueMapInSandbox?=?new?Map();
??/**?持續(xù)記錄更新的(新增和修改的)全局變量的?map,用于在任意時(shí)刻做?snapshot?*/
??currentUpdatedPropsValueMap?=?new?Map();
??name;
??proxy;
??type?=?'LegacyProxy';
??sandboxRunning?=?true;
??latestSetProp?=?null;
??active()?{
????if?(!this.sandboxRunning)?{
??????this.currentUpdatedPropsValueMap.forEach((v,?p)?=>?setWindowProp(p,?v));
????}
????this.sandboxRunning?=?true;
??}
??inactive()?{
????//?console.log('?this.modifiedPropsOriginalValueMapInSandbox',?this.modifiedPropsOriginalValueMapInSandbox)
????//?console.log('?this.addedPropsMapInSandbox',?this.addedPropsMapInSandbox)
????//刪除添加的屬性,修改已有的屬性
????this.modifiedPropsOriginalValueMapInSandbox.forEach((v,?p)?=>?setWindowProp(p,?v));
????this.addedPropsMapInSandbox.forEach((_,?p)?=>?setWindowProp(p,?undefined,?true));
????this.sandboxRunning?=?false;
??}
??constructor(name)?{
????this.name?=?name;
????const?{
??????addedPropsMapInSandbox,
??????modifiedPropsOriginalValueMapInSandbox,
??????currentUpdatedPropsValueMap
????}?=?this;
????const?rawWindow?=?window;
????//Object.create(null)的方式,傳入一個(gè)不含有原型鏈的對(duì)象
????const?fakeWindow?=?Object.create(null);?
????const?proxy?=?new?Proxy(fakeWindow,?{
??????set:?(_,?p,?value)?=>?{
????????if?(this.sandboxRunning)?{
??????????if?(!rawWindow.hasOwnProperty(p))?{
????????????addedPropsMapInSandbox.set(p,?value);
??????????}?else?if?(!modifiedPropsOriginalValueMapInSandbox.has(p))?{
????????????//?如果當(dāng)前?window?對(duì)象存在該屬性,且?record?map?中未記錄過(guò),則記錄該屬性初始值
????????????const?originalValue?=?rawWindow[p];
????????????modifiedPropsOriginalValueMapInSandbox.set(p,?originalValue);
??????????}
??????????currentUpdatedPropsValueMap.set(p,?value);
??????????//?必須重新設(shè)置?window?對(duì)象保證下次?get?時(shí)能拿到已更新的數(shù)據(jù)
??????????rawWindow[p]?=?value;
??????????this.latestSetProp?=?p;
??????????return?true;
????????}
????????//?在?strict-mode?下,Proxy?的?handler.set?返回?false?會(huì)拋出?TypeError,在沙箱卸載的情況下應(yīng)該忽略錯(cuò)誤
????????return?true;
??????},
??????get(_,?p)?{
????????//避免使用?window.window?或者?window.self?逃離沙箱環(huán)境,觸發(fā)到真實(shí)環(huán)境
????????if?(p?===?'top'?||?p?===?'parent'?||?p?===?'window'?||?p?===?'self')?{
??????????return?proxy;
????????}
????????const?value?=?rawWindow[p];
????????return?getTargetValue(rawWindow,?value);
??????},
??????has(_,?p)?{?//返回boolean
????????return?p?in?rawWindow;
??????},
??????getOwnPropertyDescriptor(_,?p)?{
????????const?descriptor?=?Object.getOwnPropertyDescriptor(rawWindow,?p);
????????//?如果屬性不作為目標(biāo)對(duì)象的自身屬性存在,則不能將其設(shè)置為不可配置
????????if?(descriptor?&&?!descriptor.configurable)?{
??????????descriptor.configurable?=?true;
????????}
????????return?descriptor;
??????},
????});
????this.proxy?=?proxy;
??}
}
let?sandbox?=?new?SingularProxySandbox();
((window)?=>?{
??window.name?=?'張三';
??window.age?=?18;
??window.sex?=?'男';
??console.log(window.name,?window.age,window.sex)?//????張三,18,男
??sandbox.inactive()?//????還原
??console.log(window.name,?window.age,window.sex)?//????張三,undefined,undefined
??sandbox.active()?//????激活
??console.log(window.name,?window.age,window.sex)?//????張三,18,男
})(sandbox.proxy);?//testlegacySandBox 還是會(huì)操作 window 對(duì)象,但是他通過(guò)激活沙箱時(shí)還原子應(yīng)用的狀態(tài),卸載時(shí)還原主應(yīng)用的狀態(tài)來(lái)實(shí)現(xiàn)沙箱隔離,同樣會(huì)對(duì) window 造成污染,但是性能比快照沙箱好,不用遍歷 window 對(duì)象。
proxySandbox(多例沙箱)
在 qiankun 的沙箱 proxySandbox 源碼里面是對(duì) fakeWindow 這個(gè)對(duì)象進(jìn)行了代理,而這個(gè)對(duì)象是通過(guò) createFakeWindow 方法得到的,這個(gè)方法是將 window 的 document、location、top、window 等等屬性拷貝一份,給到 fakeWindow。
源碼展示:
function?createFakeWindow(global:?Window)?{
??//?map?always?has?the?fastest?performance?in?has?check?scenario
??//?see?https://jsperf.com/array-indexof-vs-set-has/23
??const?propertiesWithGetter?=?new?Map<PropertyKey,?boolean>();
??const?fakeWindow?=?{}?as?FakeWindow;
??/*
???copy?the?non-configurable?property?of?global?to?fakeWindow
???see?https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/getOwnPropertyDescriptor
???>?A?property?cannot?be?reported?as?non-configurable,?if?it?does?not?exists?as?an?own?property?of?the?target?object?or?if?it?exists?as?a?configurable?own?property?of?the?target?object.
???*/
??Object.getOwnPropertyNames(global)
????.filter((p)?=>?{
??????const?descriptor?=?Object.getOwnPropertyDescriptor(global,?p);
??????return?!descriptor?.configurable;
????})
????.forEach((p)?=>?{
??????const?descriptor?=?Object.getOwnPropertyDescriptor(global,?p);
??????if?(descriptor)?{
????????const?hasGetter?=?Object.prototype.hasOwnProperty.call(descriptor,?'get');
????????/*
?????????make?top/self/window?property?configurable?and?writable,?otherwise?it?will?cause?TypeError?while?get?trap?return.
?????????see?https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/get
?????????>?The?value?reported?for?a?property?must?be?the?same?as?the?value?of?the?corresponding?target?object?property?if?the?target?object?property?is?a?non-writable,?non-configurable?data?property.
?????????*/
????????if?(
??????????p?===?'top'?||
??????????p?===?'parent'?||
??????????p?===?'self'?||
??????????p?===?'window'?||
??????????(process.env.NODE_ENV?===?'test'?&&?(p?===?'mockTop'?||?p?===?'mockSafariTop'))
????????)?{
??????????descriptor.configurable?=?true;
??????????/*
???????????The?descriptor?of?window.window/window.top/window.self?in?Safari/FF?are?accessor?descriptors,?we?need?to?avoid?adding?a?data?descriptor?while?it?was
???????????Example:
????????????Safari/FF:?Object.getOwnPropertyDescriptor(window,?'top')?->?{get:?function,?set:?undefined,?enumerable:?true,?configurable:?false}
????????????Chrome:?Object.getOwnPropertyDescriptor(window,?'top')?->?{value:?Window,?writable:?false,?enumerable:?true,?configurable:?false}
???????????*/
??????????if?(!hasGetter)?{
????????????descriptor.writable?=?true;
??????????}
????????}
????????if?(hasGetter)?propertiesWithGetter.set(p,?true);
????????//?freeze?the?descriptor?to?avoid?being?modified?by?zone.js
????????//?see?https://github.com/angular/zone.js/blob/a5fe09b0fac27ac5df1fa746042f96f05ccb6a00/lib/browser/define-property.ts#L71
????????rawObjectDefineProperty(fakeWindow,?p,?Object.freeze(descriptor));
??????}
????});
??return?{
????fakeWindow,
????propertiesWithGetter,
??};
}proxySandbox 由于是拷貝復(fù)制了一份 fakeWindow,不會(huì)污染全局 window,同時(shí)支持多個(gè)子應(yīng)用同時(shí)加載。
詳細(xì)源碼請(qǐng)查看:proxySandbox[14]
二、Node.js中的沙箱實(shí)現(xiàn)
VM
VM是 Node.js 默認(rèn)提供的一個(gè)內(nèi)建模塊,VM 模塊提供了一系列 API 用于在 V8 虛擬機(jī)環(huán)境中編譯和運(yùn)行代碼。JavaScript 代碼可以被編譯并立即運(yùn)行,或編譯、保存然后再運(yùn)行。
const?vm?=?require('vm');
const?script?=?new?vm.Script('m?+?n');?//?先new一個(gè)腳本執(zhí)行的容器實(shí)例
const?sandbox?=?{?m:?1,?n:?2?};?
const?context?=?new?vm.createContext(sandbox);?//?實(shí)例化一個(gè)執(zhí)行上下文
const?res?=?script.runInContext(context);?//?運(yùn)行
console.log(res);?//?打印:3執(zhí)行上這的代碼就能拿到結(jié)果 3,同時(shí),通過(guò) vm.Script 還能指定代碼執(zhí)行的「最大毫秒數(shù)」,超過(guò)指定的時(shí)長(zhǎng)將終止執(zhí)行并拋出一個(gè)異常:
try?{
??const?script?=?new?vm.Script('while(true){}',{?timeout:?50?});
??....
}?catch?(err){
??//?執(zhí)行超過(guò)了50ms會(huì)打印超時(shí)的?log
??console.log(err.message);
}上面的腳本執(zhí)行將會(huì)失敗,被檢測(cè)到超時(shí)并拋出異常,然后被 Try Cache 捕獲到并打出 log,但同時(shí)需要注意的是 vm.Script 的 timeout 選項(xiàng)「只針對(duì)同步代有效」,而不包括是異步調(diào)用的時(shí)間,比如
??const?script?=?new?vm.Script('setTimeout(()=>{},2000)',{?timeout:?50?});上述代碼,并不是會(huì)在 50ms 后拋出異常,因?yàn)?50ms 上邊的代碼同步執(zhí)行肯定完了,而 setTimeout 所用的時(shí)間并不算在內(nèi),也就是說(shuō) vm 模塊沒(méi)有辦法對(duì)異步代碼直接限制執(zhí)行時(shí)間。我們也不能額外通過(guò)一個(gè) timer 去檢查超時(shí),因?yàn)闄z查了執(zhí)行中的 vm 也沒(méi)有方法去中止掉。
另外,在 Node.js 通過(guò) vm.runInContext 看起來(lái)似乎隔離了代碼執(zhí)行環(huán)境,但實(shí)際上卻很容易「逃逸」出去。我們看下這個(gè)過(guò)程。
使用VM模塊我們可以在獨(dú)立的環(huán)境中運(yùn)行不受信任的代碼,這就意味著運(yùn)行在沙箱里的代碼不能訪問(wèn)Node進(jìn)程了,對(duì)嗎?
基本的使用示例代碼:
"use?strict";
const?vm?=?require("vm");
const?xyz?=?vm.runInNewContext(`let?a?=?"welcome!";a;`);
console.log(xyz);?//?a現(xiàn)在我們嘗試訪問(wèn)進(jìn)程
"use?strict";
const?vm?=?require("vm");
const?xyz?=?vm.runInNewContext(`process`);
console.log(xyz);
“process is not defined”,所以默認(rèn)情況下VM模塊不能訪問(wèn)主進(jìn)程,如果想要訪問(wèn)需要指定授權(quán)。
看起來(lái)默認(rèn)不能訪問(wèn)“process、require”等就滿足需求了,但是真的就沒(méi)有辦法觸及主進(jìn)程并執(zhí)行代碼了?
看下面的例子:
"use?strict";
const?vm?=?require("vm");
const?xyz?=?vm.runInNewContext(`this.constructor.constructor('return?this.process.env')()`);
console.log(xyz);在javascript中this指向它所屬的對(duì)象,所以我們使用它時(shí)就已經(jīng)指向了一個(gè)VM上下文之外的對(duì)象。那么訪問(wèn)this的.constructor 就返回 Object Constructor ,訪問(wèn) Object Constructor 的 .constructor 返回 Function constructor 。
Function constructor 就像javascript提供的最高函數(shù),他可以訪問(wèn)全局,所以他能返回全局事物。Function constructor允許從字符串生成函數(shù),從而執(zhí)行任意代碼。
所以我們可以使用 Function constructor 返回主進(jìn)程。關(guān)于 Function constructor 更多內(nèi)容在這里[15]和這里[16]。
可以正常打印,也就是說(shuō)順利拿到了主進(jìn)程的process,也就是上面所說(shuō)的產(chǎn)生了「逃逸」。這招同樣對(duì)突破Angular同樣有效 ——?AngularJS 沙箱[17]。
再看下面的例子:
const?vm?=?require('vm');
const?sandbox?=?{};
const?script?=?new?vm.Script('this.constructor.constructor("return?process")().exit()');
const?context?=?vm.createContext(sandbox);
script.runInContext(context);執(zhí)行上邊的代碼,宿主程序立即就會(huì)「退出」,sandbox 是在 VM 之外的環(huán)境創(chuàng)建的,需 VM 中的代碼的 this 指向的也是 sandbox,那么
//this.constructor?就是外所的?Object?構(gòu)建函數(shù)
const?ObjConstructor?=?this.constructor;?
//ObjConstructor?的?constructor?就是外包的?Function
const?Function?=?ObjConstructor.constructor;
//創(chuàng)建一個(gè)函數(shù),并執(zhí)行它,返回全局?process?全局對(duì)象
const?process?=?(new?Function('return?process'))();?
//退出當(dāng)前進(jìn)程
process.exit();?沒(méi)有人愿意用戶一段腳本就能讓?xiě)?yīng)用掛掉吧。除了退出進(jìn)程序之外,實(shí)際上還能干更多的事情。
有個(gè)簡(jiǎn)單的方法就能避免通過(guò) this.constructor 拿到 process,如下:
const?vm?=?require('vm');
//創(chuàng)建一外無(wú)?proto?的空白對(duì)象作為?sandbox
//?const?sandbox?=?{};?//?能通過(guò)this.constructor?拿到?process
const?sandbox?=?Object.create(null);?//?這樣就能防止this.constructor?拿到?process
const?script?=?new?vm.Script('this.constructor.constructor("return?process")()');
const?context?=?vm.createContext(sandbox);
const?nodeProcess?=?script.runInContext(context);
console.log(nodeProcess);但還是有風(fēng)險(xiǎn)的,由于 JavaScript 本身的動(dòng)態(tài)的特點(diǎn),各種黑魔法防不勝防。事實(shí) Node.js 的官方文檔中也提到「 不要把 VM 當(dāng)做一個(gè)安全的沙箱,去執(zhí)行任意非信任的代碼」。
VM2
在社區(qū)中有一些開(kāi)源的模塊用于運(yùn)行不信任代碼,例如 sandbox、vm2、jailed 等。相比較而言 vm2 對(duì)各方面做了更多的安全工作,相對(duì)安全些。「這也是為什么imageCook采用了該沙箱模塊」
從 vm2 的官方 README 中可以看到,它基于 Node.js 內(nèi)建的 VM 模塊,來(lái)建立基礎(chǔ)的沙箱環(huán)境,然后同時(shí)使用上了文介紹過(guò)的 ES6 的 Proxy 技術(shù)來(lái)防止沙箱腳本逃逸。
用同樣的測(cè)試代碼來(lái)試試 vm2:
const?{?VM?}?=?require('vm2');
new?VM().run('this.constructor.constructor("return?process")().exit()');如上代碼,并沒(méi)有成功結(jié)束掉宿主程序,vm2 官方 REAME 中說(shuō)「vm2 是一個(gè)沙盒,可以在 Node.js 中安全的執(zhí)行不受信任的代碼」。

然而,事實(shí)上我們還是可以干一些「壞」事情,比如:
只要能干壞事情,就是不安全的
const?{?VM?}?=?require('vm2');
const?vm?=?new?VM({?timeout:?1000,?sandbox:?{}});
vm.run('new?Promise(()=>{})');上邊的代碼將永遠(yuǎn)不會(huì)執(zhí)行結(jié)束,如同 Node.js 內(nèi)建模塊一樣,vm2 的 timeout 對(duì)異步操作是無(wú)效的。同時(shí),vm2 也不能額外通過(guò)一個(gè) timer 去檢查超時(shí),因?yàn)樗矝](méi)有辦法將執(zhí)行中的 vm 終止掉。這會(huì)一點(diǎn)點(diǎn)耗費(fèi)完服務(wù)器的資源,讓你的應(yīng)用掛掉。
那么或許你會(huì)想,我們能不能在上邊的 sandbox 中放一個(gè)假的 Promise 從而禁掉 Promise 呢?答案是能提供一個(gè)「假」的 Promise,但卻沒(méi)有辦法完成禁掉 Promise,比如
const?{?VM?}?=?require('vm2');
const?vm?=?new?VM({?
??timeout:?1000,?
??sandbox:?{?Promise:?function(){}}
});
vm.run('Promise?=?(async?function(){})().constructor;new?Promise(()=>{});');可以看到通過(guò)一行 Promise = (async function(){})().constructor 就可以輕松再次拿到 Promise 了。從另一個(gè)層面來(lái)看,況且或許有時(shí)我們還想讓自定義腳本支持異步處理呢。
關(guān)于VM2還有更多新的和創(chuàng)新性的繞過(guò) ——更多逃逸[18]。
除了從沙箱逃逸,還可以使用 infinite while loop 創(chuàng)建無(wú)限循環(huán)拒絕服務(wù)。
const?{VM}?=?require('vm2');
new?VM({timeout:1}).run(`
????function?main(){
????????while(1){}
????}
????
????new?Proxy({},?{
????????getPrototypeOf(t){
????????????global.main();
????????}
????})`
);Safeify[19]:Node.js環(huán)境下建立一個(gè)更安全的沙箱
通過(guò)上文的探究,我們并沒(méi)有找到一個(gè)完美的方案在 Node.js 建立安全的隔離的沙箱。其中 vm2 做了不少處理,相對(duì)來(lái)講算是較安全的方案了,但問(wèn)題也很明顯,比如異步不能檢查超時(shí)的問(wèn)題以及和宿主程序在相同進(jìn)程的問(wèn)題。
沒(méi)有進(jìn)程隔離時(shí),通過(guò) VM 創(chuàng)建的 sanbox 大體是這樣的

那么,我們是不是可以嘗試,將非受信代碼,通過(guò) vm2 這個(gè)模塊隔離在一個(gè)獨(dú)立的進(jìn)程中執(zhí)行呢?然后,執(zhí)行超時(shí)時(shí),直接將隔離的進(jìn)程干掉,但這里我們需要考慮如下幾個(gè)問(wèn)題:
?通過(guò)進(jìn)程池統(tǒng)一調(diào)度管理沙箱進(jìn)程
如果來(lái)一個(gè)執(zhí)行任務(wù),創(chuàng)建一個(gè)進(jìn)程,用完銷(xiāo)毀,僅處理進(jìn)程的開(kāi)銷(xiāo)就已經(jīng)稍大了,并且也不能不設(shè)限的開(kāi)新進(jìn)程和宿主應(yīng)用搶資源,那么,需要建一個(gè)進(jìn)程池:
前提:所有任務(wù)到來(lái)會(huì)創(chuàng)建一個(gè) Script 實(shí)例,先進(jìn)入一個(gè) pending 隊(duì)列,然后直接將 script 實(shí)例的 defer 對(duì)象返回,調(diào)用處就能 await 執(zhí)行結(jié)果了
然后:由 sandbox master 根據(jù)工程進(jìn)程的空閑程序來(lái)調(diào)度執(zhí)行,master 會(huì)將 script 的執(zhí)行信息,包括重要的 ScriptId,發(fā)送給空閑的 worker,worker 執(zhí)行完成后會(huì)將「結(jié)果 + script 信息」回傳給 master,master 通過(guò) ScriptId 識(shí)別是哪個(gè)腳本執(zhí)行完畢了,就是結(jié)果進(jìn)行 resolve 或 reject 處理。
這樣,通過(guò)「進(jìn)程池」既能降低「進(jìn)程來(lái)回創(chuàng)建和銷(xiāo)毀的開(kāi)銷(xiāo)」,也能確保不過(guò)度搶占宿主資源;同時(shí),在異步操作超時(shí),還能將工程進(jìn)程直接殺掉;同時(shí),master 將發(fā)現(xiàn)一個(gè)工程進(jìn)程掛掉,會(huì)立即創(chuàng)建替補(bǔ)進(jìn)程。
?處理的數(shù)據(jù)和結(jié)果公開(kāi)給沙箱的方法
進(jìn)程間如何通訊,需要「動(dòng)態(tài)代碼」操作數(shù)據(jù)后可以直接序列化然后通過(guò) IPC 發(fā)送給隔離 Sandbox 進(jìn)程,執(zhí)行結(jié)果一樣經(jīng)過(guò)序列化通過(guò) IPC 傳輸。
其中,如果想公開(kāi)一個(gè)方法給 sandbox,因?yàn)椴辉谝粋€(gè)進(jìn)程,并不能方便的將一個(gè)方案的引用傳遞給 sandbox。我們可以將宿主的方法,在傳遞給 sandbox worker 之類(lèi)做一下處理,轉(zhuǎn)換為一個(gè)「描述對(duì)象」,包括了允許 sandbox 調(diào)用的方法信息,然后將信息,如同其它數(shù)據(jù)一樣發(fā)送給 worker 進(jìn)程,worker 收到數(shù)據(jù)后,識(shí)別出「方法描述對(duì)象」,然后在 worker 進(jìn)程中的 sandbox 對(duì)象上建立代理方法,代理方法同樣通過(guò) IPC 和 master 通訊。
?針對(duì)沙箱進(jìn)程進(jìn)行 CPU 和內(nèi)存配額限制
在 Linux 平臺(tái),通過(guò) CGroups 對(duì)沙箱進(jìn)程進(jìn)行整體的 CPU 和內(nèi)存等資源的配額限制,CGroups 是 Control Groups 的縮寫(xiě),是 Linux 內(nèi)核提供的一種可以限制、記錄、隔離進(jìn)程組(Process Groups)所使用的物理資源(如:CPU、Memory、IO 等等)的機(jī)制。最初由 Google 的工程師提出,后來(lái)被整合進(jìn) Linux 內(nèi)核。CGroups 也是 LXC 為實(shí)現(xiàn)虛擬化所使用的資源管理手段,可以說(shuō)沒(méi)有 CGroups 就沒(méi)有 LXC。
最終,我們建立了一個(gè)大約這樣的「沙箱環(huán)境」

如此這般處理起來(lái)是不是感覺(jué)很麻煩?但我們就有了一個(gè)更加安全一些的沙箱環(huán)境了,基于這些處理被封裝為一個(gè)獨(dú)立的模塊?Safeify[20],在Github上已經(jīng)開(kāi)源。
相較于內(nèi)建的 VM 及常見(jiàn)的幾個(gè)沙箱模塊, Safeify 具有如下特點(diǎn):
?為將要執(zhí)行的動(dòng)態(tài)代碼建立專(zhuān)門(mén)的進(jìn)程池,與宿主應(yīng)用程序分離在不同的進(jìn)程中執(zhí)行?支持配置沙箱進(jìn)程池的最大進(jìn)程數(shù)量?支持限定同步代碼的最大執(zhí)行時(shí)間,同時(shí)也支持限定包括異步代碼在內(nèi)的執(zhí)行時(shí)間?支持限定沙箱進(jìn)程池的整體的 CPU 資源配額(小數(shù))?支持限定沙箱進(jìn)程池的整體的最大的內(nèi)存限制(單位 m)
簡(jiǎn)單介紹一下 Safeify 如何使用,通過(guò)如下命令安裝
npm?i?safeify?--save在應(yīng)用中使用,還是比較簡(jiǎn)單的,如下代碼(TypeScript 中類(lèi)似)
import?{?Safeify?}?from?'safeify';
const?safeVm?=?new?Safeify({
??timeout:?50,??????????//超時(shí)時(shí)間,默認(rèn)?50ms
??asyncTimeout:?500,????//包含異步操作的超時(shí)時(shí)間,默認(rèn)?500ms
??quantity:?4,??????????//沙箱進(jìn)程數(shù)量,默認(rèn)同?CPU?核數(shù)
??memoryQuota:?500,?????//沙箱最大能使用的內(nèi)存(單位?m),默認(rèn)?500m
??cpuQuota:?0.5,????????//沙箱的?cpu?資源配額(百分比),默認(rèn)?50%
});
const?context?=?{
??a:?1,?
??b:?2,
??add(a,?b)?{
????return?a?+?b;
??}
};
const?rs?=?await?safeVm.run(`return?add(a,?b)`,?context);
console.log('result',rs);關(guān)于安全的問(wèn)題,沒(méi)有最安全,只有更安全。Safeify 已在部分項(xiàng)目中使用,但自定義腳本的功能是往往僅針對(duì)內(nèi)網(wǎng)用戶,有不少動(dòng)態(tài)執(zhí)行代碼的場(chǎng)景其實(shí)是可以避免的,繞不開(kāi)或?qū)嵲谛枰峁┻@個(gè)功能時(shí),希望本文或 Safeify 能對(duì)大家有所幫助就行了。
結(jié)論
運(yùn)行不信任的代碼是非常困難的,只依賴軟件模塊作為沙箱技術(shù),防止不受信任代碼用于非正當(dāng)用途是不得已的決定。這可能促使云上SAAS應(yīng)用的不安全,因?yàn)橥ㄟ^(guò)逃逸出沙箱進(jìn)程多個(gè)租戶間的數(shù)據(jù)可能被訪問(wèn)(主進(jìn)程數(shù)據(jù)獲取),這樣你就可能可以通過(guò)session,secret等來(lái)潛入其他租戶。一個(gè)更安全的選擇是依賴于硬件虛擬化,比如每個(gè)租戶代碼在獨(dú)立的docker容器或AWS Lambada Function 中執(zhí)行會(huì)是更好的選擇。
下面是Auth0如何處理沙箱問(wèn)題:Sandboxing Node.js with CoreOS and Docker[21]。「下來(lái)可以再詳細(xì)研究下實(shí)現(xiàn)」
三、看一個(gè)case
imageCook的使用case

目標(biāo):拿到用于前端頁(yè)面渲染的index.js + index.css
基本思路:
?模板代碼生成代碼:https://github.com/imgcook-dsl/react-xt-standard/blob/master/src/index.js?基于Group/倉(cāng)庫(kù)名可以拿到整個(gè)倉(cāng)庫(kù)的所有代碼?gitlab的代碼拉取實(shí)現(xiàn)方式可以參考:針對(duì)字節(jié)現(xiàn)狀封裝的Gitlab API[22]?「使用了Node.js的混合流」?github的代碼拉取可以參考:https://www.npmjs.com/package/download-git-repo 曾被vue-cli 2.x[23]版本使用
{
????"package.json":?"xxx",
????"src/index.js":?"yyy"
}?拿到執(zhí)行函數(shù)字符串
module.exports?=?function(schema,?option)?{
??let?imgNumber?=?0;
??const?{prettier}?=?option;
??...
??};?Node.js沙箱執(zhí)行,得到上面函數(shù)返回的字符串
import?{?Safeify?}?from?'safeify';
import?{?getRepoProjectEntries?}?from?'byte-gitlab';
const?safeVm?=?new?Safeify({
??timeout:?50,??????????//?超時(shí)時(shí)間,默認(rèn)?50ms
??asyncTimeout:?500,????//?包含異步操作的超時(shí)時(shí)間,默認(rèn)?500ms
??quantity:?4,??????????//?沙箱進(jìn)程數(shù)量,默認(rèn)同?CPU?核數(shù)
??memoryQuota:?500,?????//?沙箱最大能使用的內(nèi)存(單位?m),默認(rèn)?500m
??cpuQuota:?0.5,????????//?沙箱的?cpu?資源配額(百分比),默認(rèn)?50%
});
const?context?=?{
???schema:?{},?
???option:?{}
};
(async?()?=>?{
??const?zipStream?=?await?getRepoProjectEntries({
????group:?'mordor',
????project:?'lynx-standard',
????branch:?'master'
??});
??
??zipStream
????.pipe(async?(contents:?string,?path:?string)?=>?{
????????const?rs?=?await?safeVm.run(contents,?context);
????????console.log('result',?rs);
????????
????????return?rs;
????})
????.pipe(this.emitDone())
????.once("done",?done)
????.once("error",?(err)?=>?{
??????console.log("流執(zhí)行出錯(cuò)統(tǒng)一監(jiān)控:".red,?err);
????});
})();?返回給客戶端
關(guān)于 CSS 隔離
常見(jiàn)的有,不再贅述:?CSS Module?namespace?Dynamic StyleSheet?css in js?Shadow DOM
引用鏈接
[1]?沙箱(Sandbox):?http://www.arkteam.net/?p=2967[2]?JavaScript中constructor屬性:?https://segmentfault.com/a/1190000013245739[3]?class:?https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/class[4]?Class構(gòu)造方法:?https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Classes/constructor[5]?Object.getPrototypeOf():?https://www.axihe.com/api/js-es/ob-object/get-prototype-of.html[6]?Reflect.getPrototypeOf():?https://www.axihe.com/api/js-es/ob-reflect/get-prototype-of.html[7]?proto:?https://www.axihe.com/api/js-es/ob-object/proto.html[8]?Object.prototype.isPrototypeOf():?https://www.axihe.com/api/js-es/ob-object/is-prototype-of.html[9]?instanceof:?https://www.axihe.com/api/js-es/ex-relational/instanceof.html[10]?TypeError:?https://www.axihe.com/api/js-es/ob-error/type-error.html[11]?專(zhuān)訪 Wind.js 作者老趙(上):緣由、思路及發(fā)展:?https://www.infoq.cn/article/interview-jscex-author-part-1[12]?(0, eval)(‘this’):?https://www.cnblogs.com/qianlegeqian/p/3950044.html[13]?qiankun:?https://qiankun.umijs.org/zh/guide[14]?:proxySandbox:?https://link.segmentfault.com/?enc=Mb%2BNNJjUrmTA7g2uf%2FTgzQ%3D%3D.IHwAeHwf8%2FPDd3WJLo%2F4dCWf2md2lzw7s%2BIEdUcUHmX7xMSccEguXX%2BFQBtpgU8SHiyqxgnCi00SvzmT95eNTRD1XaOHjO5xokQrsy%2BHYtQ%3D[15]?這里:?https://link.juejin.cn/?target=http%3A%2F%2Fdfkaye.github.io%2F2014%2F03%2F14%2Fjavascript-eval-and-function-constructor%2F[16]?這里:?https://link.juejin.cn/?target=https%3A%2F%2Fgithub.com%2Fi0natan%2Fnodebestpractices%2Fissues%2F211[17]?AngularJS 沙箱:?https://link.juejin.cn/?target=https%3A%2F%2Fportswigger.net%2Fresearch%2Fdom-based-angularjs-sandbox-escapes[18]?更多逃逸:?https://github.com/patriksimek/vm2/issues?q=is%3Aissue+author%3AXmiliaH+is%3Aclosed[19]?Safeify:?https://github.com/Houfeng/safeify[20]?Safeify:?https://github.com/Houfeng/safeify[21]?Sandboxing Node.js with CoreOS and Docker:?https://link.juejin.cn/?target=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3Du81pS05W1JY
