日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
一文徹底搞懂前端沙箱

什么是“沙箱”

有哪些動態(tài)執(zhí)行腳本的場景?

在一些應(yīng)用中,我們希望給用戶提供插入自定義邏輯的能力,比如 Microsoft 的 Office 中的 VBA,比如一些游戲中的 lua 腳本,F(xiàn)ireFox 的「油猴腳本」,能夠讓用戶發(fā)在可控的范圍和權(quán)限內(nèi)發(fā)揮想象做一些好玩、有用的事情,擴(kuò)展了能力,滿足用戶的個性化需求。

大多數(shù)都是一些客戶端程序,在一些在線的系統(tǒng)和產(chǎn)品中也常常也有類似的需求,事實上,在線的應(yīng)用中也有不少提供了自定義腳本的能力,比如 Google Docs 中的 Apps Script,它可以讓你使用 JavaScript 做一些非常有用的事情,比如運(yùn)行代碼來響應(yīng)文檔打開事件或單元格更改事件,為公式制作自定義電子表格函數(shù)等等。

與運(yùn)行在「用戶電腦中」的客戶端應(yīng)用不同,用戶的自定義腳本通常只能影響用戶自已,而對于在線的應(yīng)用或服務(wù)來講,有一些情況就變得更為重要,比如「安全」,用戶的「自定義腳本」必須嚴(yán)格受到限制和隔離,即不能影響到宿主程序,也不能影響到其它用戶。

另外,有一些牽扯「模板化」的前端框架,如Vue.js、Venom.js等都會用到動態(tài)代碼執(zhí)行。

JavaScript中的沙箱實現(xiàn)

1.幾個基礎(chǔ)知識

什么是constructor

  • JavaScript中constructor屬性指向創(chuàng)建當(dāng)前對象的構(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)建的對象的特殊方法 Class構(gòu)造方法[4]。

幾個典型的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()是一個代理方法,當(dāng)讀取代理對象的原型時,該方法就會被調(diào)用。語法:

const p = new Proxy(obj, {
getPrototypeOf(target) { // target 被代理的目標(biāo)對象。
...
}
});

當(dāng) getPrototypeOf 方法被調(diào)用時,this 指向的是它所屬的處理器對象,getPrototypeOf 方法的返回值必須是一個對象或者 null。

在 JavaScript 中,有下面這五種操作(方法/屬性/運(yùn)算符)可以觸發(fā) JS 引擎讀取一個對象的原型,也就是可以觸發(fā) getPrototypeOf() 代理方法的運(yùn)行:

  • Object.getPrototypeOf()[5]
  • Reflect.getPrototypeOf()[6]
  • proto[7]
  • Object.prototype.isPrototypeOf()[8]
  • instanceof[9]

如果遇到了下面兩種情況,JS 引擎會拋出 TypeError[10] 異常:

  • getPrototypeOf() 方法返回的不是對象也不是 null。
  • 目標(biāo)對象是不可擴(kuò)展的,且 getPrototypeOf() 方法返回的原型不是目標(biāo)對象本身的原型。

基本用法:

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是被代理的對象,也就是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() 方法返回的不是對象也不是 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)對象是不可擴(kuò)展的,且 getPrototypeOf() 方法返回的原型不是目標(biāo)對象本身的原型
const obj = Object.preventExtensions({}); // obj不可擴(kuò)展
const p = new Proxy(obj, {
getPrototypeOf(target) {
return {};
}
});
Object.getPrototypeOf(p); // TypeError: expected same prototype value

// 如果對上面的代碼做如下的改造就沒問題
const obj = Object.preventExtensions({}); // obj不可擴(kuò)展
const p = new Proxy(obj, {
getPrototypeOf(target) { // target就是上面的obj
return obj.__proto__; // 返回的是目標(biāo)對象本身的原型
}
});
Object.getPrototypeOf(p); // 不報錯

2.跟瀏覽器宿主環(huán)境一致的沙箱實現(xiàn)

構(gòu)建閉包環(huán)境

我們知道在 JavaScript 中的作用域(scope)只有全局作用域(global scope)、函數(shù)作用域(function scope)以及從 ES6 開始才有的塊級作用域(block scope)。如果要將一段代碼中的變量、函數(shù)等的定義隔離出來,受限于 JavaScript 對作用域的控制,只能將這段代碼封裝到一個 Function 中,通過使用 function scope 來達(dá)到作用域隔離的目的。也因為需要這種使用函數(shù)來達(dá)到作用域隔離的目的方式,于是就有 IIFE(立即調(diào)用函數(shù)表達(dá)式),這是一個被稱為“自執(zhí)行匿名函數(shù)”的設(shè)計模式。

(function foo(){
const a = 1;
console.log(a);
})();// 無法從外部訪問變量

console.log(a) // 拋出錯誤:"Uncaught ReferenceError: a is not defined"

當(dāng)函數(shù)變成立即執(zhí)行的函數(shù)表達(dá)式時,表達(dá)式中的變量不能從外部訪問,它擁有獨(dú)立的詞法作用域。不僅避免了外界訪問 IIFE 中的變量,而且又不會污染全局作用域,彌補(bǔ)了 JavaScript 在 scope 方面的缺陷。一般常見于寫插件和類庫時,如 JQuery 當(dāng)中的沙箱模式

(function (window) {
var jQuery = function (selector, context) {
return new jQuery.fn.init(selector, context);
}
jQuery.fn = jQuery.prototype = function () {
//原型上的方法,即所有jQuery對象都可以共享的方法和屬性
}
jQuery.fn.init.prototype = jQuery.fn;
window.jQeury = window.$ = jQuery; //如果需要在外界暴露一些屬性或者方法,可以將這些屬性和方法加到window全局對象上去
})(window);

當(dāng)將 IIFE 分配給一個變量,不是存儲 IIFE 本身,而是存儲 IIFE 執(zhí)行后返回的結(jié)果。

const result = (function () {
const name = "張三";
return name;
})();

console.log(result); // "張三"

原生瀏覽器對象的模擬

模擬原生瀏覽器對象的目的是為了防止閉包環(huán)境,操作原生對象,篡改污染原生環(huán)境,完成模擬瀏覽器對象之前我們需要先關(guān)注幾個不常用的 API。

eval

eval 函數(shù)可將字符串轉(zhuǎn)換為代碼執(zhí)行,并返回一個或多個值:

const b = eval("({name:'張三'})");
console.log(b.name);

由于 eval 執(zhí)行的代碼可以訪問閉包和全局范圍,因此就導(dǎo)致了代碼注入的安全問題,因為代碼內(nèi)部可以沿著作用域鏈往上找,篡改全局變量,這是我們不希望的。

console.log(eval( this.window === window )); // true

補(bǔ)充幾個點(diǎn):

  • 性能&安全問題,一般不建議在實際業(yè)務(wù)代碼中引入eval
  • 輔助異步編程框架的windjs大量采用eval的寫法來輔助編程
  • 瀏覽器環(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)建一個新的 Function 對象。直接調(diào)用這個構(gòu)造函數(shù)可用于動態(tài)創(chuàng)建函數(shù)。

new Function ([arg1[, arg2[, ...argN]],] functionBody)

arg1, arg2, ... argN 被函數(shù)使用的參數(shù)的名稱必須是合法命名的。參數(shù)名稱是一個有效的 JavaScript 標(biāo)識符的字符串,或者一個用逗號分隔的有效字符串的列表,例如“×”,“theValue”,或“a,b”。

補(bǔ)充幾個點(diǎn):

  • new Function()性能一般比eval要好,很多用到這塊的前端框架都是用new Function()實現(xiàn)的,比如:Vue.js。
  • 打開瀏覽器控制臺后,new Function()的性能要慢一倍以上。

functionBody

一個含有包括函數(shù)定義的 JavaScript 語句的字符串。

const sum = new Function('a', 'b', 'return a + b'); 
console.log(sum(1, 2));//3

同樣也會遇到和 eval 類似的的安全問題和相對較小的性能問題。

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)行,它無法訪問局部閉包變量,它們總是被創(chuàng)建于全局環(huán)境,因此在運(yùn)行時它們只能訪問全局變量和自己的局部變量,不能訪問它們被 Function 構(gòu)造器創(chuàng)建時所在的作用域的變量。new Function()是 eval()更好替代方案。它具有卓越的性能和安全性,但仍沒有解決訪問全局的問題。

with

with 是 JavaScript 中一個關(guān)鍵字,擴(kuò)展一個語句的作用域鏈。它允許半沙盒執(zhí)行。那什么叫半沙盒?語句將某個對象添加到作用域鏈的頂部,如果在沙盒中有某個未使用命名空間的變量,跟作用域鏈中的某個屬性同名,則這個變量將指向這個屬性值。如果沒有同名的屬性,則將拋出 ReferenceError。

function sandbox(o) {
with (o){
//a=5;
c=2;
d=3;
console.log(a,b,c,d); // 0,1,2,3 //每個變量首先被認(rèn)為是一個局部變量,如果局部變量與 obj 對象的某個屬性同名,則這個局部變量會指向 obj 對象屬性。
}
}

const f = {
a:0,
b:1
}
sandbox(f);

console.log(f);
console.log(c,d); // 2,3 c、d被泄露到window對象上

究其原理,with在內(nèi)部使用in運(yùn)算符。對于塊內(nèi)的每個變量訪問,它都在沙盒條件下計算變量。如果條件是 true,它將從沙盒中檢索變量。否則,就在全局范圍內(nèi)查找變量。但是 with 語句使程序在查找變量值時,都是先在指定的對象中查找。所以對于那些本來不是這個對象的屬性的變量,查找起來會很慢,對于有性能要求的程序不適合(JavaScript 引擎會在編譯階段進(jìn)行數(shù)項的性能優(yōu)化。其中有些優(yōu)化依賴于能夠根據(jù)代碼的詞法進(jìn)行靜態(tài)分析,并預(yù)先確定所有變量和函數(shù)的定義位置,才能在執(zhí)行過程中快速找到標(biāo)識符)。with 也會導(dǎo)致數(shù)據(jù)泄漏(在非嚴(yán)格模式下,會自動在全局作用域創(chuàng)建一個全局變量)。

in 運(yùn)算符

in 運(yùn)算符能夠檢測左側(cè)操作數(shù)是否為右側(cè)操作數(shù)的成員。其中,左側(cè)操作數(shù)是一個字符串,或者可以轉(zhuǎn)換為字符串的表達(dá)式,右側(cè)操作數(shù)是一個對象或數(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 提供對象查找,但是如果查找不到依然還能從更上面的作用域獲取,污染或篡改全局環(huán)境。

function sandbox (src) {
src = 'with (sandbox) {' + src + '}';
return new Function('sandbox', src);
}

const str = `
let a = 1;
window.name="張三";
console.log(a); // 打?。?
`;

sandbox(str)({});

console.log(window.name);//'張三'

可以看到,基于上面的方案都多多少少存在一些安全問題:

  • eval 是全局對象的一個函數(shù)屬性,執(zhí)行的代碼擁有著和應(yīng)用中其它正常代碼一樣的的權(quán)限,它能訪問「執(zhí)行上下文」中的局部變量,也能訪問所有「全局變量」,在這個場景下,它是一個非常危險的函數(shù)。
  • 使用 Function 構(gòu)造器生成的函數(shù),并不會在創(chuàng)建它的上下文中創(chuàng)建閉包,一般在全局作用域中被創(chuàng)建。當(dāng)運(yùn)行函數(shù)的時候,只能訪問自己的本地變量和全局變量,不能訪問 Function 構(gòu)造器被調(diào)用生成的上下文的作用域。
  • with 一樣的問題,它首先會在傳入的對象中查找對應(yīng)的變量,如果找不到就會往更上層的全局作用域去查找,所以也避免不了污染或篡改全局環(huán)境。

那有沒有更安全一些的沙箱環(huán)境實現(xiàn)呢?

基于 Proxy 實現(xiàn)的沙箱(ProxySandbox)

ES6 Proxy 用于修改某些操作的默認(rèn)行為,等同于在語言層面做出修改,屬于一種“元編程”(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) {
// 讓動態(tài)執(zhí)行的代碼認(rèn)為屬性已存在
return true;
}
});
return fn(proxy);
}
evalute('1+2') // 3
evalute('console.log(1)') // Cannot read property 'log' of undefined

我們知道無論 eval 還是 function,執(zhí)行時都會把作用域一層一層向上查找,如果找不到會一直到 global,那么利用 Proxy 的原理就是,讓執(zhí)行了代碼在 sandobx 中找的到,以達(dá)到「防逃逸」的目的。

我們前面提到with在內(nèi)部使用in運(yùn)算符來計算變量,如果條件是 true,它將從沙盒中檢索變量。理想狀態(tài)下沒有問題,但也總有些特例獨(dú)行的存在,比如 Symbol.unscopables。

Symbol 對象的 Symbol.unscopables 屬性,指向一個對象。該對象指定了使用 with 關(guān)鍵字時,哪些屬性會被 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']

上面代碼說明,數(shù)組有 6 個屬性,會被 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 定義對象的不可作用屬性。Unscopeable 屬性永遠(yuǎn)不會從 with 語句中的沙箱對象中檢索,而是直接從閉包或全局范圍中檢索。

快照沙箱(SnapshotSandbox)

快照沙箱實現(xiàn)來說比較簡單,主要用于不支持 Proxy 的低版本瀏覽器,原理是基于diff來實現(xiàn)的,在子應(yīng)用激活或者卸載時分別去通過快照的形式記錄或還原狀態(tài)來實現(xiàn)沙箱,snapshotSandbox 會污染全局 window。

我們看下 qiankun[13] 的 snapshotSandbox 的源碼,這里為了幫助理解做部分精簡及注釋。

function iter(obj, callbackFn) {
for (const prop in obj) {
if (obj.hasOwnProperty(prop)) {
callbackFn(prop);
}
}
}

/**
* 基于 diff 方式實現(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 沙箱實現(xià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ù)對象,如 window.console、window.atob 這類。目前沒有完美的檢測方式,這里通過 prototype 中是否還有可枚舉的拓展方法的方式來判斷
@warning 這里不要隨意替換成別的判斷方式,因為可能觸發(fā)一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于調(diào)用了 top window 對象觸發(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 實現(xiàn)的沙箱
*/
class SingularProxySandbox {
/** 沙箱期間新增的全局變量 */
addedPropsMapInSandbox = new Map();

/** 沙箱期間更新的全局變量 */
modifiedPropsOriginalValueMapInSandbox = new Map();

/** 持續(xù)記錄更新的(新增和修改的)全局變量的 map,用于在任意時刻做 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)的方式,傳入一個不含有原型鏈的對象
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 對象存在該屬性,且 record map 中未記錄過,則記錄該屬性初始值
const originalValue = rawWindow[p];
modifiedPropsOriginalValueMapInSandbox.set(p, originalValue);
}

currentUpdatedPropsValueMap.set(p, value);
// 必須重新設(shè)置 window 對象保證下次 get 時能拿到已更新的數(shù)據(jù)
rawWindow[p] = value;

this.latestSetProp = p;

return true;
}

// 在 strict-mode 下,Proxy 的 handler.set 返回 false 會拋出 TypeError,在沙箱卸載的情況下應(yīng)該忽略錯誤
return true;
},

get(_, p) {
//避免使用 window.window 或者 window.self 逃離沙箱環(huán)境,觸發(fā)到真實環(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)對象的自身屬性存在,則不能將其設(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 還是會操作 window 對象,但是他通過激活沙箱時還原子應(yīng)用的狀態(tài),卸載時還原主應(yīng)用的狀態(tài)來實現(xiàn)沙箱隔離,同樣會對 window 造成污染,但是性能比快照沙箱好,不用遍歷 window 對象。

proxySandbox(多例沙箱)

在 qiankun 的沙箱 proxySandbox 源碼里面是對 fakeWindow 這個對象進(jìn)行了代理,而這個對象是通過 createFakeWindow 方法得到的,這個方法是將 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();
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,不會污染全局 window,同時支持多個子應(yīng)用同時加載。

詳細(xì)源碼請查看:proxySandbox[14]。

3.Node.js中的沙箱實現(xiàn)

VM

VM是 Node.js 默認(rèn)提供的一個內(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一個腳本執(zhí)行的容器實例
const sandbox = { m: 1, n: 2 };
const context = new vm.createContext(sandbox); // 實例化一個執(zhí)行上下文
const res = script.runInContext(context); // 運(yùn)行
console.log(res); // 打?。?

執(zhí)行上這的代碼就能拿到結(jié)果 3,同時,通過 vm.Script 還能指定代碼執(zhí)行的「最大毫秒數(shù)」,超過指定的時長將終止執(zhí)行并拋出一個異常:

try {
const script = new vm.Script('while(true){}',{ timeout: 50 });
....
} catch (err){
// 執(zhí)行超過了50ms會打印超時的 log
console.log(err.message);
}

上面的腳本執(zhí)行將會失敗,被檢測到超時并拋出異常,然后被 Try Cache 捕獲到并打出 log,但同時需要注意的是 vm.Script 的 timeout 選項「只針對同步代有效」,而不包括是異步調(diào)用的時間,比如:

const script = new vm.Script('setTimeout(()=>{},2000)',{ timeout: 50 });

上述代碼,并不是會在 50ms 后拋出異常,因為 50ms 上邊的代碼同步執(zhí)行肯定完了,而 setTimeout 所用的時間并不算在內(nèi),也就是說 vm 模塊沒有辦法對異步代碼直接限制執(zhí)行時間。我們也不能額外通過一個 timer 去檢查超時,因為檢查了執(zhí)行中的 vm 也沒有方法去中止掉。

另外,在 Node.js 通過 vm.runInContext 看起來似乎隔離了代碼執(zhí)行環(huán)境,但實際上卻很容易「逃逸」出去。我們看下這個過程。

使用VM模塊我們可以在獨(dú)立的環(huán)境中運(yùn)行不受信任的代碼,這就意味著運(yùn)行在沙箱里的代碼不能訪問Node進(jìn)程了,對嗎?

基本的使用示例代碼:

"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`let a = "welcome!";a;`);
console.log(xyz); // a

現(xiàn)在我們嘗試訪問進(jìn)程:

"use strict";
const vm = require("vm");
const xyz = vm.runInNewContext(`let a = "welcome!";a;`);
console.log(xyz); // a

“process is not defined”,所以默認(rèn)情況下VM模塊不能訪問主進(jìn)程,如果想要訪問需要指定授權(quán)。

看起來默認(rèn)不能訪問“process、require”等就滿足需求了,但是真的就沒有辦法觸及主進(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指向它所屬的對象,所以我們使用它時就已經(jīng)指向了一個VM上下文之外的對象。那么訪問this的.constructor 就返回 Object Constructor ,訪問 Object Constructor 的 .constructor 返回 Function constructor 。

Function constructor 就像javascript提供的最高函數(shù),他可以訪問全局,所以他能返回全局事物。Function constructor允許從字符串生成函數(shù),從而執(zhí)行任意代碼。

所以我們可以使用 Function constructor 返回主進(jìn)程。關(guān)于 Function constructor 更多內(nèi)容在這里[15]和這里[16]。

可以正常打印,也就是說順利拿到了主進(jìn)程的process,也就是上面所說的產(chǎn)生了「逃逸」。這招同樣對突破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í)行上邊的代碼,宿主程序立即就會「退出」,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)建一個函數(shù),并執(zhí)行它,返回全局 process 全局對象
const process = (new Function('return process'))();
//退出當(dāng)前進(jìn)程
process.exit();

沒有人愿意用戶一段腳本就能讓應(yīng)用掛掉吧。除了退出進(jìn)程序之外,實際上還能干更多的事情。

有個簡單的方法就能避免通過 this.constructor 拿到 process,如下:

const vm = require('vm');
//創(chuàng)建一外無 proto 的空白對象作為 sandbox
// const sandbox = {}; // 能通過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)險的,由于 JavaScript 本身的動態(tài)的特點(diǎn),各種黑魔法防不勝防。事實 Node.js 的官方文檔中也提到「 不要把 VM 當(dāng)做一個安全的沙箱,去執(zhí)行任意非信任的代碼」。

VM2

在社區(qū)中有一些開源的模塊用于運(yùn)行不信任代碼,例如 sandbox、vm2、jailed 等。相比較而言 vm2 對各方面做了更多的安全工作,相對安全些?!高@也是為什么imageCook采用了該沙箱模塊」

從 vm2 的官方 README 中可以看到,它基于 Node.js 內(nèi)建的 VM 模塊,來建立基礎(chǔ)的沙箱環(huán)境,然后同時使用上了文介紹過的 ES6 的 Proxy 技術(shù)來防止沙箱腳本逃逸。

用同樣的測試代碼來試試 vm2:

const { VM } = require('vm2');
new VM().run('this.constructor.constructor("return process")().exit()');

如上代碼,并沒有成功結(jié)束掉宿主程序,vm2 官方 REAME 中說「vm2 是一個沙盒,可以在 Node.js 中安全的執(zhí)行不受信任的代碼」。

然而,事實上我們還是可以干一些「壞」事情,比如:

只要能干壞事情,就是不安全的

const { VM } = require('vm2');
const vm = new VM({ timeout: 1000, sandbox: {}});
vm.run('new Promise(()=>{})');

上邊的代碼將永遠(yuǎn)不會執(zhí)行結(jié)束,如同 Node.js 內(nèi)建模塊一樣,vm2 的 timeout 對異步操作是無效的。同時,vm2 也不能額外通過一個 timer 去檢查超時,因為它也沒有辦法將執(zhí)行中的 vm 終止掉。這會一點(diǎn)點(diǎn)耗費(fèi)完服務(wù)器的資源,讓你的應(yīng)用掛掉。

那么或許你會想,我們能不能在上邊的 sandbox 中放一個假的 Promise 從而禁掉 Promise 呢?答案是能提供一個「假」的 Promise,但卻沒有辦法完成禁掉 Promise,比如:

const { VM } = require('vm2');
const vm = new VM({
timeout: 1000,
sandbox: { Promise: function(){}}
});

vm.run('Promise = (async function(){})().constructor;new Promise(()=>{});');

可以看到通過一行 Promise = (async function(){})().constructor 就可以輕松再次拿到 Promise 了。從另一個層面來看,況且或許有時我們還想讓自定義腳本支持異步處理呢。

關(guān)于VM2還有更多新的和創(chuàng)新性的繞過 ——更多逃逸[18]。

除了從沙箱逃逸,還可以使用 infinite while loop 創(chuàng)建無限循環(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)境下建立一個更安全的沙箱

通過上文的探究,我們并沒有找到一個完美的方案在 Node.js 建立安全的隔離的沙箱。其中 vm2 做了不少處理,相對來講算是較安全的方案了,但問題也很明顯,比如異步不能檢查超時的問題以及和宿主程序在相同進(jìn)程的問題。

沒有進(jìn)程隔離時,通過 VM 創(chuàng)建的 sanbox 大體是這樣的

http://www.5511xx.com/article/dhgcooj.html