動手實現一個 JavaScript 沙箱

沙箱的存在不僅是爲了安全問題,也是爲了解決一些隔離性的問題,這裏只考慮隔離性問題,不考慮惡意注入。要爲了安全隔離惡意代碼的話,請使用 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

  • eval 會影響調用的上下文
  • geval 不會影響上下文,可是會直接在全局做用域下執行,變量等會掛到全局
  • 嚴格 eval 能夠讀寫上下文的變量,可是不能新增,代碼執行爲嚴格模式
  • 嚴格 geval 同上,可是在全局做用域下執行
  • Function 至關於在全局做用域下建立一個匿名函數執行

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 中,不過還有些問題:

  1. 能夠直接經過 window、self、this、globalThis 來訪問全局變量,並影響全局屬性
  2. 經過拿到一些全局屬性的引用後能夠篡改全局屬性的值
  3. Function('return this') function(){return this} 和 eval('this') 能夠拿到真實的 window

第一個點比較好解決,訪問這些屬性時直接返回 proxy 就好了,this 能夠經過將 Function bind proxy 解決 第二個就比較麻煩了,因爲全局變量不少都是引用類型,要解決除非一層層深克隆(要處理各類奇怪問題),或者一層層代理(也會出現各類各樣的問題),因此放棄了,畢竟篡改全局變量不是什麼好代碼,通常場景下也不多出現這樣的代碼,不過咱們能夠經過白名單或者黑名單的方式,讓沙盒中的代碼只能訪問必要的全局變量,防止重要的全局變量被篡改

我能怎麼辦
第三個也很麻煩,Function 和間接 eval 是直接在全局下執行的,實在想解決的話,Function 和 eval 能夠經過拋出自定義的 eval 和 Function 來實現,而 function 的話能夠經過啓用沙箱的嚴格模式來實現

然而仍是能夠繞過,好比使用 (function(){}).constructor

使用場景

上面能夠看出來,在面對惡意代碼時,使用 JavaScript 自己去實現的沙箱是沒法絕對安全的(甚至沒考慮防注入),不過這個不是很安全的沙箱也有它的使用場景,好比面對內部代碼雖然安全,可是又不可控的全局變量可能會致使代碼間互相影響而致使 crash 的,好比須要在同一個頁面運行多個版本庫的(正常會相互衝突)

舉個🌰

想看 DEMO 效果的能夠直接看這裏: (這個圖片是能夠點的)

Edit quirky-microservice-8oqog

效果基本如期,其中還有一些比較細節實現,有興趣的能夠關注下最終實現庫,源碼不到 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

參考文獻

writing-a-javascript-framework-sandboxed-code-evaluation/

相關文章
相關標籤/搜索