什么是 JS 沙箱
一个独立的、隔离的、不会被外界影响的 JS 的运行环境。
JS 沙箱的使用场景
- JSONP:由于 JSONP 往往是被动接受 JS 内容,所以可能会有一定程度上的安全风险,这个时候可以使用沙箱来运行 JSONP 返回的 JS 代码。
- 执行第三方 JS:当我们需要执行第三方 JS 模块,但第三方 JS 模块也不定安全的时候可以使用沙箱来运行第三方 JS 模块。
- 在线代码编辑器:在在线代码编辑器领域,特别是 JS 相关编辑器,处于性能的考虑往往会将用户输入的 JS 代码在前端执行,这个时候为了防止污染站点自身的 JS 环境则需要一个沙箱来保证安全性。
- 表达式计算:这一类与上一个基本相同,在用户输入的一些可以由 JS 来执行内容以确定内容结果或者正确性的场景下,则需要一个 JS 沙箱来保证安全性和独立性。
- 微前端:子应用与子应用之间的 JS 隔离,防止应用之间出现污染。
基本可以归类为一下三类抽象场景:
- 要解析或执行不可信的JS的时候。
- 要隔离被执行代码的执行环境的时候。
- 要对执行代码中可访问对象进行限制的时候。
JS 沙箱的常见解决方案
Web Workers
API 文档:https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Workers_API/Using_web_workers
Web Workers 是笔者最初接触 JS 沙箱第一个想到的方案之一,它是浏览器原生提供的创建独立 JS 线程的 API,天然具备独立性,兼容性自然也是极好的。但是有一个问题就是它并不与主 JS 线程完全有一致,它们的全局变量不一致,DOM 操作也不允许。所以仅能成为承担工作相对简单的 JS 沙箱。比如对一些简单表达式的计算等。
with() + new Function()
with 提供作用域欺骗,new Function 进行上下文注入并执行代码,这个时候可以提供一个操作受限或者被监控的 fakeWindow 作为上下文。
with + new Function 相对于 Web Worker 多了一分灵活性,它尝试在主 JS 线程下模拟一个新的上下文来执行 JS 代码,由于我们可以针对新的上下文 fakeWindow 做很多文章,从而使得该方案的 JS 沙箱所能承担的责任具备相当的弹性。
比如我们可以设置 fakeWindow 为真实 Window 的某个子集,从而使得沙箱可以操作部分 Window 上的方法或者属性,形成一个逃逸舱。
function createSandBox (code, fakeWindow) {
genSandBoxRunner(code).call(fakeWindow, fakeWindow)
}
function genSandBoxRunner (code: string): Function {
const withCode = `with(fakeWindow) { ${code} }`
return new Function('fakeWindow', withCode)
}
该方案缺点其实很明显,就是 with 的作用域欺骗会导致 v8 无法对运行在沙箱内的代码进行性能优化,从而导致一定程度上的性能浪费。代码量越大,性能浪费越多。
同时还有一些安全风险,这一方案典型的只防君子不防小人,比如说:
- code 中可以提前关闭 sandbox 的
with
语境,如'} alert(this); {'
- code 中可以使用
eval
和new Function
直接逃逸 - 等等等
这一策略已经基本可以在相当多的场景下使用,主要对 fakeWindow 的控制在不同场景下会呈现出不同的复杂度,这一点需要注意。
Iframe + contentWindow
阿里云微前端采用的 JS 沙箱策略:https://github.com/aliyun/alibabacloud-alfa/tree/master/packages/core/browser-vm
该方案从同源的 iframe 实例中获得一个新的 contentWindow 来座位运行上下文,但执行手段上来说还是上述的 with + new Function,所以从某种角度上来说可以视为上一种方案的补充,不需要十分麻烦的去封装出一个 fakeWindow,直接拿一个现成的就行,但 with + new Fucntion 的一些问题依旧存在,虽然相对而言安全风险会少一点。
具体可以看官方博客:阿里云开放平台微前端方案的沙箱实现
proxySandBox
著名微前端解决方案之一乾坤的多实例沙箱解决方案:https://github.com/umijs/qiankun/blob/master/src/sandbox/proxySandbox.ts#L128
主要思路是挟持 window,利用 Proxy 代理应用在 window 上的操作,缓存每个应用自身操作所产生的的值,当应用获取值时则优先返回被当前应用修改的值。
执行 JS 的时候则是使用 eval 和 iife 来进行作用域欺骗和上下文注入,和 with + new Function 感觉差大不大。
snapshotSandBox
乾坤的兼容性解决方案:https://github.com/umijs/qiankun/blob/master/src/sandbox/snapshotSandbox.ts#L21
该方案相对粗糙,回先给原始 window 打个快照,当应用失活的时候 diff 一下原始 window 和使用过后的 window,将不同的地方记录一下并还原,等到激活应用的时候再将不同的地方 patch 回来
Realm API
提案:tc39/proposal-realms: ECMAScript Proposal, specs, and reference implementation for Realms Shim:Agoric/realms-shim: Spec-compliant shim for Realms TC39 Proposal
Realm 是一个尚在 Stage3 的新提案,Realms提议提供一种在新的全局对象和一组JavaScript内置的上下文中执行JavaScript代码的新机制。大抵来说就是提议出一个第一方的沙箱机制,相当的简单粗暴(笑
Portals
提案:WICG/portals: A proposal for enabling seamless navigations between sites or pages
一个类似于 iframe 的新标签提案,笔者看了下提案,大概意思是一个不需要频繁刷新的 iframe。
参考
再谈沙箱:前端所涉及的沙箱细讲
iframe - sandbox 属性 - 《阮一峰 HTML 语言教程》 - 书栈网 · BookStack
构建一个安全的 JavaScript 沙箱
阿里云开放平台微前端方案的沙箱实现
万字长文+图文并茂+全面解析微前端框架 qiankun 源码 - qiankun 篇