沙箱的存在不仅是为了安全问题,也是为了解决一些隔离性的问题,这里只考虑隔离性问题,不考虑恶意注入。要为了安全隔离恶意代码的话,请使用 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 阻断变量的查询,就能将对变量的访问锁死在沙盒环境中。然而,报错了。markdown
因为阻断了变量的查询,全局对象上的正常属性也都没法访问了,这就不妙了。如何在阻断后还能访问到全局变量呢,把咱们上面的 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
考虑到各类上述的各类实现上的问题,以及还有不少由于篡改了 window 致使的方法错误的问题,改版后的最终实现看这里:github.com/ZxBing0066/…
上面能够看出来,在面对恶意代码时,使用 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