沙箱实现
沙箱实现步骤一般如下:
- 解析代码,动态执行
- 修改代码的作用域,避免进行向上全局查询
- 创建全局对象的替代对象,避免污染全局对象
- 执行代码
基于Proxy实现
前面基础知识将到Proxy是一个可以代理对象的方法,那么其实可以按照将一些全局对象做代理后放入到沙箱里。主要有两个步骤:
- 使用
new Function
将代码字符串转为可执行函数 - 加
with
生成局部作用域 - 使用
createFake
方法生成替代对象fakeWindow
- 使用
Proxy
代理拦截set
get
操作,更新到fakeWindow
中 - 当沙箱不用时,将
window
重置回
简易沙箱示例代码如下:
const { complieCode2Fn, createFakeWindow } = require('./complieCode.js'); class ProxySandbox { constructor(global) { const rawGlobal = global; const fakeWindow = createFakeWindow(global); this.proxyBox = new Proxy(fakeWindow, { // 拦截所有属性,防止到 Proxy 对象以外的作用域链查找。 has(target, key) { return true; }, get(target, key, receiver) { console.log('get', key, target[key]) // 加固,防止逃逸 if (key === Symbol.unscopables) { return undefined; } // 通过Reflect获取 let temp = Reflect.get(target, key, receiver); if(!target.hasOwnProperty(key)){ temp = rawGlobal[key]; } return temp; }, set(target, key, newValue) { if (!target.hasOwnProperty(key) && fakeWindow.hasOwnProperty(key)) { const descriptor = Object.getOwnPropertyDescriptor(sandbox, key); const { writable, configurable, enumerable } = descriptor; if (writable) { // 中独有的属性如果可以写,同样需要复制到fakeWindow中 Object.defineProperty(target, p, { configurable, enumerable, writable, newValue, }); } } else { target[key] = newValue; } return true; } }); return this; } excute(code){ const fn = complieCode2Fn(code); return fn(this.proxyBox); } } const fakeBox = new ProxySandbox(global); const code = `a = 1;console.log('a:', a);return a;` console.log(fakeBox.excute(code)); // 输出a:1 console.log('在沙箱外获取沙箱内设置的全局值a:', a); // a is not defined
问题1: 如何防止Array.isArray
重写后不影响顶部window?
```js const code = ` Array.isArray = ()=> true; ` console.log(Array.isArray('a')); // 输出true 正常应该是false
生成fakeWindow
对象时候,遍历内置Array
,通过Object.freeze
冻结其修改的可能性
问题2: 提前关闭 sandbox 的 with 语境,如 '} alert(this); {' 或者使用 eval 和 new Function 直接逃逸,如何解决?
解析code字符串,利用堆栈深度检测算法,将非法字符串 {}
做简单计算 或者 eval
等关键字,然后报错处理
问题3: 如何解决修改原型链方法实现逃逸,既可以获取沙箱外的对象?
const code = ` ({}).constructor.prototype.toString = () => { console.log('Escape') } ` console.log(({}).toString()) // 输出Escape 正常应该输出[object Object]
这种只能在原型链上下功夫,将所有的原型链做一次封装,从而
基于iframe实现
利用iframe
天然隔离机制,加上postMessage
通讯机制,可以快速实现一个简易沙箱,具体步骤如下:
- 创建一个iframe,获取其window作为替代对象
- 将function执行放到iframe里,不会影响其沙箱外程序使用
class IframeSandbox { constructor() { // 创建一个 iframe 对象,取出其中的原生浏览器全局对象作为沙箱的全局对象 const iframe = document.createElement('iframe', {url: 'about:blank'}) document.body.appendChild(iframe) this.sandboxGlobal = iframe.contentWindow // 沙箱运行时的全局对象 } excute(code){ const fn = new this.sandboxGlobal.Function('sandbox', `with(sandbox){${code}}`) return fn(this.sandboxGlobal); } } const fakeBox = new IframeSandbox(); const code = `a = 1;console.log(a)` console.log(fakeBox.excute(code)); // 输出a:1 console.log('在沙箱外获取沙箱内设置的全局值a:', a); // a is not defined
问题1: 需要解决其调用parent
进行逃逸获取?
- 最佳方案是通过
Proxy
对iframe的window对象进行拦截代理即可
基于ShadowRealm 提案的实现
ShadowRealm API 是一个新的 JavaScript 提案,它允许一个 JS 运行时创建多个高度隔离的 JS 运行环境(realm),每个 realm 具有独立的全局对象和内建对象。
通俗的说,这是JavaScript自带的沙箱API,你可以利用它快速实现上面需要通过proxy或iframe才能实现的隔离机制。
ShadowRealm声明:
declare class ShadowRealm { constructor(); evaluate(sourceText: string): PrimitiveValueOrCallable; importValue(specifier: string, bindingName: string): Promise<PrimitiveValueOrCallable>; }
evaluate(sourceText: string)
同步执行代码字符串,类似 eval()importValue(specifier: string, bindingName: string)
异步执行代码字符串
示例:
const sr = new ShadowRealm(); globalThis.test = 'test'; sr.evaluate(`globalThis.test = 'test ShadowRealm'; console.log(globalThis.test)`) // 输出 test ShadowRealm // 创建一个文件 my-module.js export function sum(...values) { return values.reduce((prev, value) => prev + value); } // main.js const sr = new ShadowRealm(); const wrappedSum = await sr.importValue('./my-module.js', 'sum'); // 加载js模块,然后获取里面函数 console.log(wrappedSum('hi', ' ', 'folks', '!')); // 输出 hi folks !
其实再来实现一个沙箱就很简单了,因为ShadownRealm本身就是一个沙箱。
其他方案
Web Workers
Web Workers
代码运行在独立的进程中,通信是异步的,无法获取当前程序一些属性或共享状态,且有一点无法不支持 DOM 操作,必须通过 postMessage 通知 UI 主线程来实现
function workerSandbox(appCode) { var blob = new Blob([appCode]); var appWorker = new Worker(window.URL.createObjectURL(blob)); } workerSandbox('const a = 1;console.log(a);') // 输出1 console.log(a) // a not defined
vm 模块
Node.js 上的 vm 模块,与 ShadowRealm API 类似,但具有更多功能:缓存 JavaScript 引擎、拦截 import() 等等。但它唯一的缺点就是不能跨平台,只能在 Node.js 环境下使用。
const vm = require('vm'); const sandbox = { a: 1 }; vm.createContext(sandbox) const whatIsThis = vm.runInContext(` a = 2 ; `, sandbox); console.log(sandbox) // 输出2
沙箱错误捕获
在完成沙箱主体后,还需要对沙箱内部错误进行捕获再次处理,从而不影响主体程序的执行。
这一块其实就在执行动态代码那里,做一层try/catch
基本上可以完成的错误捕获。
沙箱逃逸
沙箱逃逸(Sandbox Escape),沙箱于作者而言是一种安全策略,但于使用者而言可能是一种束缚。脑洞大开的开发者们尝试用各种方式摆脱这种束缚,也称之为沙箱逃逸。
沙箱逃逸的几种方式:
- 访问沙箱执行上下文中某个对象内部属性时,如:通过window.parent
- 通过访问原型链实现逃逸
如何解决沙箱逃逸:
自定义解释器,分析源程序结构从而手动控制每一条语句的执行逻辑,如:
Babel
等
简单的说,就是用JS去实现JS解释器,将每行代码进行解析,然后增加一些安全机制,从而避免非法代码入侵。
后续会专门写个文章去实现一个简单的JS解释器,这里就不做多阐述。更新后会放在这里链接。