JS 沙箱技术探索

技术 5月 03, 2021

什么是 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 中可以使用 evalnew 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。

参考

拓展阅读

浅探 Web Worker 与 JavaScript 沙箱

Pengsha Ying

逝者如斯,故不舍昼夜