前言
沙箱,即 sandbox,顧名思義,就是讓你的程式跑在一個隔離的環境下,不對外界的其他程式造成影響,透過建立類似沙盒的獨立作業環境,在其內部執行的程式並不能對硬碟產生永久性的影響。
JS 中沙箱的使用場景
jsonp:解析伺服器所返回的 jsonp 請求時,如果不信任 jsonp 中的資料,可以透過建立沙箱的方式來解析獲取資料;(TSW 中處理 jsonp 請求時,建立沙箱來處理和解析資料);執行第三方 js:當你有必要執行第三方 js 的時候,而這份 js 檔案又不一定可信的時候;
線上程式碼編輯器:相信大家都有使用過一些線上程式碼編輯器,而這些程式碼的執行,基本都會放置在沙箱中,防止對頁面本身造成影響;(例如:https://codesandbox.io/s/new)
vue 的服務端渲染:vue 的服務端渲染實現中,透過建立沙箱執行前端的 bundle 檔案;在呼叫 createBundleRenderer 方法時候,允許配置 runInNewContext 為 true 或 false 的形式,判斷是否傳入一個新建立的 sandbox 物件以供 vm 使用;
vue 模板中表示式計算:vue 模板中表示式的計算被放在沙盒中,只能訪問全域性變數的一個白名單,如 Math 和 Date 。你不能夠在模板表示式中試圖訪問使用者定義的全域性變數。
實現方式
基於 iframe 的沙箱環境實現
在前端,最常見的方法還是使用 iframe 來構造一個沙箱。iframe 本身就是一個封閉的沙箱環境,假如你要執行的程式碼不是自己寫的程式碼,不是可信的資料來源,那麼可以使用 iframe 來執行。
const parent = window;
const frame = document.createElement('iframe');
// 限制程式碼 iframe 程式碼執行能力
frame.sandbox = 'allow-same-origin';
const data = [1, 2, 3, 4, 5, 6];
let newData = [];
// 當前頁面給 iframe 傳送訊息
frame.onload = function (e) {
frame.contentWindow.postMessage(data);
};
document.body.appendChild(frame);
// iframe 接收到訊息後處理
const code = `
return dataInIframe.filter((item) => item % 2 === 0)
`;
frame.contentWindow.addEventListener('message', function (e) {
const func = new frame.contentWindow.Function('dataInIframe', code);
// 給副頁面也送訊息
parent.postMessage(func(e.data));
});
// 父頁面接收 iframe 傳送過來的訊息
parent.addEventListener(
'message',
function (e) {
console.log('parent - message from iframe:', e.data);
},
false,
);
關於 iframe sandbox 的更多介紹:
github.com/xitu/gold-miner/blob/ma...
相關實現庫:
基於 Proxy 的沙箱環境實現
現在主流的另一種沙箱使用的是 with + Proxy 來實現沙箱。該方法常用於 js 隔離,如微前端框架便是透過該方法實現 js 隔離,從而是微應用間不產生干擾。
with 關鍵字
JavaScript 在查詢某個未使用名稱空間的變數時,會透過作用於鏈來查詢,而 with
關鍵字,可以使得查詢時,先從該物件的屬性開始查詢,若該物件沒有要查詢的屬性,順著上一級作用域鏈查詢,若不存在要查到的屬性,則會返回 ReferenceError
異常。
不推薦使用 with
,在 ECMAScript 5 嚴格模式中該標籤已被禁止。推薦的替代方案是宣告一個臨時變數來承載你所需要的屬性。
效能方面的利與弊
- 利:with 語句可以在不造成效能損失的情況下,減少變數的長度。其造成的附加計算量很少。使用 ‘with’ 可以減少不必要的指標路徑解析運算。需要注意的是,很多情況下,也可以不使用 with 語句,而是使用一個臨時變數來儲存指標,來達到同樣的效果。
- 弊:with 語句使得程式在查詢變數值時,都是先在指定的物件中查詢。所以那些本來不是這個物件的屬性的變數,查詢起來將會很慢。如果是在對效能要求較高的場合,’with’ 下面的 statement 語句中的變數,只應該包含這個指定物件的屬性
相關文件:developer.mozilla.org/zh-CN/docs/W...
ES6 Proxy
Proxy 是 ES6 提供的新語法,Proxy 物件用於建立一個物件的代理,從而實現基本操作的攔截和自定義(如屬性查詢、賦值、列舉、函式呼叫等)。示例如下:
const handler = {
get: function (obj, prop) {
return prop in obj ? obj[prop] : 37;
},
};
const p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 37
Symbol.unscopables
Symbol.unscopables
指用於指定物件值,其物件自身和繼承的從關聯物件的 with 環境繫結中排除的屬性名稱。Symbol.unscopables
設定了 true 的屬性,會無視 with
的作用域直接到上級查詢,從而造成逃逸。示例如下:
const property1 = 12;
const object1 = {
property1: 42,
};
object1[Symbol.unscopables] = {
property1: true,
};
with (object1) {
console.log(property1);
// expected output: 12
}
在 JavaScript 中,有許多預設設定了 Symbol.unscopables
的屬性。如:
Array.prototype[Symbol.unscopables];
/*{
copyWithin: true,
entries: true,
fill: true,
find: true,
findIndex: true,
flat: true,
flatMap: true,
includes: true,
keys: true,
values: true,
}*/
沙箱實現
透過上述對 with
和 Proxy
的瞭解,我們便可以構建一個可被攔截的物件,來防止沙箱內程式碼逃逸,對全域性物件造成汙染。程式碼如下:
function compileCode(code) {
code = `with (sandbox) { ${code} }`;
const fn = new Function('sandbox', code);
return (sandbox) => {
const proxy = new Proxy(sandbox, {
// 攔截所有屬性,防止到 Proxy 物件以外的作用域鏈查詢。
has(target, key) {
return true;
},
get(target, key, receiver) {
// 加固,防止逃逸
if (key === Symbol.unscopables) {
return undefined;
}
return Reflect.get(target, key, receiver);
},
});
return fn(proxy);
};
}
同時我們也可以使用 Object.freeze
來防止原型鏈被修改。
存在的問題
code
中可以提前關閉sandbox
的with
語境,如'} alert(this); {';
code
中可以使用eval
和new Function
直接逃逸
由於以上的問題目前並未找到較合適的解決方法,因此該方式並不適合執行 不可信任的第三方程式碼
。
微前端框架 qiankun
的沙箱原理:
juejin.cn/post/6920110573418086413
仍在提案中的 SES
該特性是還在提案中的特性,但是已經可以在大多數引擎中使用了,它支援 ESM
模組呼叫,也可以直接透過 <script>
直接引入使用。
該特性主要是透過 Object.freeze
來隔離出安全沙箱,從而安全地執行第三方程式碼,使用方法如下:
<script src="https://unpkg.com/ses" charset="utf-8"></script>
<script>
const c = new Compartment();
const code = `
(function () {
const arr = [1, 2, 3, 4];
return arr.filter(x => x > 2);
})
`;
const fn = c.evaluate(code);
console.log(arr); // ReferenceError: arr is not defined
console.log(fn()); // [3, 4]
</script>
相關文件:
由於該特性仍在提案中,因此未來改動可能會比較大,例如,最初是由 iframe 來實現,但現在已經由 Proxy + Object.freeze
來實現了。
也得益於放棄使用 iframe
從而使得程式碼可以同步執行,不必再使用 postMessage
來非同步通訊了。
對比
實現方式 | iframe | with + Proxy | SES |
---|---|---|---|
相容性 | IE10+ | 不支援 IE | 仍在草案中 |
實現方式 | 一般 | 複雜,需要考慮許多邊界情況 | 簡單,只需要呼叫簡單的 API |
同步/非同步 | 非同步 | 同步 | 同步 |
使用場景 | 大多數需要隔離沙箱或需要執行不安全程式碼的場景 | 僅使用與需要隔離沙箱的場景 | 大多數是需要沙箱的場景。 |
總結
本次藉助要介紹了三種實現沙箱的方法,分別是 iframe
, with + Proxy
和 SES
。
但上述實現方式,均不太適合執行 不可信任的第三方程式碼
, 例如在程式碼中有無限迴圈的程式碼,由於以上方式均與主執行緒同處一個 thread
,更會造成頁面阻塞,對於該問題,有一個不太完美的解決方案。
上述提到的 jailed 庫,由於是基於 Web Worker 實現,可以避免上述死迴圈問題導致的頁面卡死。
本文首發於 個人部落格
本作品採用《CC 協議》,轉載必須註明作者和本文連結