<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          一文徹底搞懂前端沙箱

          共 29453字,需瀏覽 59分鐘

           ·

          2022-06-09 01:37

          什么是“沙箱”

          也稱(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?===?Function

          JS 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);?//test

          legacySandBox 還是會(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

          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  婷婷色综合网 | 91视频在线观看 | av电影久久 | 亚洲成人网站在线播放 | 三级在线观看视频 |