沙箱的存在不僅是爲了安全問題,也是爲了解決一些隔離性的問題,這裏只考慮隔離性問題,不考慮惡意注入。要爲了安全隔離惡意代碼的話,請使用 iframe 之類的方案解決。javascript
這幾天項目中有涉及到各項目間代碼隔離的內容,因此針對JS
中的沙箱實現作了一些嘗試,基本實現了正常代碼間的運行隔離,這裏記錄一下實現過程。css
想看下最終效果的能夠直接看下方 舉個🌰java
要實現沙箱,首先,得讓一段代碼受控的跑起來,代碼得轉成字符串,而後使用字符串調用代碼。react
這裏很容易就想到了 eval 和 Function。ios
const exec1 = code => eval(code);
const geval = eval;
const exec2 = code => geval(code);
const exec3 = code => {
'use strict';
eval(code);
};
const exec4 = code => {
'use strict';
geval(code);
};
const exec5 = code => Function(code)();
複製代碼
總共有上述 5 中方式能夠實現代碼的運行:git
geval 能夠看最下方知識點。 咱們選擇 Function 來實現(eval 也能夠實現,稍微麻煩一點,Function('code')();
基本等價於 const geval = eval; geval('function() {"code"})()');
),github
const global = this;
(function() {
let outterVariable = 'outter';
const createSandbox = () => {
return code => {
Function(` ;${code}; `)();
};
};
const sandbox = createSandbox();
sandbox(` var a = 1; var b = 2; // 期待打出 1 2 console.log(a, b); outterVariable = 'sandbox'; console.log(outterVariable); `);
try {
console.log(a, 'fail');
} catch (e) {
console.log('success');
}
try {
console.log(b, 'fail');
} catch (b) {
console.log('success');
}
console.log(outterVariable);
})();
console.log(outterVariable);
複製代碼
除了全局變量的問題,貌似一切 OK,再想一想怎麼解決全局變量這個大麻煩axios
改變代碼的做用域,除了 eval、Function 就只能想到 with 了,不過 with 的功能是將給定的表達式掛到做用域的頂端,全局變量好像不太行?等等,那試試 Proxy 呢。安全
const global = this;
(function() {
let outterVariable = 'outter';
const createSandbox = () => {
const context = {};
const proxy = new Proxy(context, {
set: (obj, prop, value) => {
console.log(prop);
obj[prop] = value
},
get: (obj, prop) => {
if(prop in obj) return obj[prop];
return undefined;
},
has: (obj, prop) => {
return true;
}
});
return code => {
Function('proxy', ` with(proxy) { ;${code}; } `)(proxy);
};
};
const sandbox = createSandbox();
sandbox(` var a = 1; var b = 2; // 期待打出 1 2 console.log(a, b); outterVariable = 'sandbox'; console.log(outterVariable); `);
try {
console.log(a, 'fail');
} catch (e) {
console.log('success');
}
try {
console.log(b, 'fail');
} catch (b) {
console.log('success');
}
console.log(outterVariable);
})();
console.log(outterVariable);
複製代碼
經過 with 改變做用域鏈,以及 Proxy 的 has 阻斷變量的查詢,就能將對變量的訪問鎖死在沙盒環境中。然而,報錯了。app
因爲阻斷了變量的查詢,全局對象上的正常屬性也都沒法訪問了,這就不妙了。如何在阻斷後還能訪問到全局變量呢,把咱們上面的 context 裏塞上 window 的屬性就好啦。固然不能一個個複製,這時候咱們能夠直接使用繼承,這樣不止能訪問到全局,還能讓對全局對象的修改隻影響到 context 而不影響 window,可喜可賀 可喜可賀。
const global = this;
(function() {
let outterVariable = 'outter';
const createSandbox = () => {
const context = Object.create(global);
const proxy = new Proxy(context, {
set: (obj, prop, value) => {
obj[prop] = value;
},
get: (obj, prop) => {
return obj[prop];
},
has: () => {
return true;
}
});
return code => {
Function(
'proxy',
` with(proxy) { ;${code}; } `
)(proxy);
};
};
const sandbox = createSandbox();
sandbox(` var a = 1; var b = 2; // 期待打出 1 2 console.log(a, b); outterVariable = 'sandbox'; console.log(outterVariable); `);
try {
console.log(a, 'fail');
} catch (e) {
console.log('success');
}
try {
console.log(b, 'fail');
} catch (b) {
console.log('success');
}
console.log(outterVariable);
})();
console.log('outterVariable' in global);
複製代碼
貌似離成功不遠了,全局變量的訪問經過原型鏈完成,變量的隔離經過 with 和 Proxy 的 has 屬性鎖死在 context 中,不過還有些問題:
第一個點比較好解決,訪問這些屬性時直接返回 proxy 就好了,this 能夠經過將 Function bind proxy 解決 第二個就比較麻煩了,因爲全局變量不少都是引用類型,要解決除非一層層深克隆(要處理各類奇怪問題),或者一層層代理(也會出現各類各樣的問題),因此放棄了,畢竟篡改全局變量不是什麼好代碼,通常場景下也不多出現這樣的代碼,不過咱們能夠經過白名單或者黑名單的方式,讓沙盒中的代碼只能訪問必要的全局變量,防止重要的全局變量被篡改
然而仍是能夠繞過,好比使用 (function(){}).constructor
上面能夠看出來,在面對惡意代碼時,使用 JavaScript 自己去實現的沙箱是沒法絕對安全的(甚至沒考慮防注入),不過這個不是很安全的沙箱也有它的使用場景,好比面對內部代碼雖然安全,可是又不可控的全局變量可能會致使代碼間互相影響而致使 crash 的,好比須要在同一個頁面運行多個版本庫的(正常會相互衝突)
想看 DEMO 效果的能夠直接看這裏: (這個圖片是能夠點的)
效果基本如期,其中還有一些比較細節實現,有興趣的能夠關注下最終實現庫,源碼不到 100 行 (這個圖片也是能夠點的)
經過下面的代碼咱們能夠很方便的將 React15 和 16 跑在一塊兒,而不須要擔憂它們互相干擾。
import "./styles.css";
import { createSandbox } from "z-sandbox";
import axios from "axios";
document.getElementById("app").innerHTML = ` <div id='container1'> </div> <div id='container2'> </div> `;
(function() {
console.log(window.screen);
const sandbox15 = createSandbox({}, { useStrict: true });
const sandbox16 = createSandbox({}, { useStrict: true });
const getReactCode15 = () =>
axios
.get("https://unpkg.com/react@15.6.2/dist/react-with-addons.js")
.then(res => res.data);
const getReactCode16 = () =>
axios
.get("https://unpkg.com/react@16.11.0/umd/react.development.js")
.then(res => res.data);
const getReactDOMCode15 = () =>
axios
.get("https://unpkg.com/react-dom@15.6.2/dist/react-dom.js")
.then(res => res.data);
const getReactDOMCode16 = () =>
axios
.get("https://unpkg.com/react-dom@16.11.0/umd/react-dom.development.js")
.then(res => res.data);
Promise.all([
getReactCode15(),
getReactCode16(),
getReactDOMCode15(),
getReactDOMCode16()
]).then(([reactCode15, reactCode16, reactDOMCode15, reactDOMCode16]) => {
console.log(
reactCode15.length,
reactCode16.length,
reactDOMCode15.length,
reactDOMCode16.length
);
sandbox15(` console.log(Object.prototype) `);
sandbox15(reactCode15);
sandbox15(reactDOMCode15);
sandbox16(reactCode16);
sandbox16(reactDOMCode16);
sandbox15(` ReactDOM.render(React.createElement('div', { onClick: () => alert('I am a component using React' + React.version) }, 'Hello world, try to click me'), document.getElementById('container1')) `);
sandbox16(` ReactDOM.render(React.createElement('div', { onClick: () => alert('I am a component using React' + React.version) }, 'Hello world, try to click me'), document.getElementById('container2')) `);
console.log(sandbox15.context.React.version);
console.log(sandbox16.context.React.version);
});
})();
複製代碼
因爲變量的攔截藉助於最新的 Proxy API,因爲兼容
If you use the eval function indirectly, by invoking it via a reference other than eval, as of ECMAScript 5 it works in the global scope rather than the local scope. This means, for instance, that function declarations create global functions, and that the code being evaluated doesn't have access to local variables within the scope where it's being called. MDN
MDN 有描述,當 間接調用 eval 時,將會在 全局環境 下執行而不會影響到做用域中的本地變量。因此通常也稱爲全局 eval