深入理解JS沙箱

什么是JS沙箱:

沙箱,即 sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。

在现实与 JavaScript 相关的场景中,我们知道平时使用的浏览器就是一个沙箱,运行在浏览器中的 JavaScript 代码无法直接访问文件系统、显示器或其他任何硬件。

Chrome 浏览器中每个标签页也是一个沙箱,各个标签页内的数据无法直接相互影响,接口都在独立的上下文中运行。而在同一个浏览器标签页下运行 HTML 页面

在开发过程中,例如『用户希望可以自己写 js 代码运行』的需求,例如『要执行用户提交的不可信任的第三方代码』等需求,都需要要利用沙箱,来防止代码对全局产生影响。

JS沙箱的使用场景:

使用沙箱需求的诸多应用场景,譬如:

  1. 执行从不受信的源获取到的第三方 JavaScript 代码时(比如引入插件、处理 jsonp 请求回来的数据等)。

解析服务器所返回的 jsonp 请求时,如果不信任 jsonp 中的数据,可以通过创建沙箱的方式来解析获取数据;(TSW 中处理 jsonp 请求时,创建沙箱来处理和解析数据);执行第三方 js:当你有必要执行第三方 js 的时候,而这份 js 文件又不一定可信的时候;

  1. 在线代码编辑器场景(比如著名的 codesandbox)。
  2. 使用服务端渲染方案。
  • vue 的服务端渲染实现中,通过创建沙箱执行前端的 bundle 文件;
  • 在调用 createBundleRenderer 方法时候,允许配置 runInNewContext 为 true 或 false 的形式,判断是否传入一个新创建的 sandbox 对象以供 vm 使用;
  • next.js也用到了沙箱,但是更复杂一些
  1. 模板字符串中的表达式的计算。

vue 模板中表达式的计算被放在沙盒中,只能访问全局变量的一个白名单,如 Math 和 Date 。你不能够在模板表达式中试图访问用户定义的全局变量。

JS沙箱的条件:

  1. 挂在 window 上的全局方法/变量(如 setTimeout、滚动等全局事件监听等)在子应用切换时的清理和还原。
  2. Cookie、LocalStorage 等的读写安全策略限制。
  3. 各子应用独立路由的实现。
  4. 多个微应用共存时相互独立的实现

JS隔离的几种方式:

谈到JS沙箱,市场上真正能落地的产品不多,从「iframe」到「single-spa」到「qiankun」,但其中的解决方案都有各自的缺点,基本都是微前端 && codebox 的场景中落地。

方案1 window快照还原

利用 window 上常用的常量和方法以及不支持 Proxy时降级通过快照实现备份还原,这一方案是比较原始的超脱方案

qiankun框架应该有这种方案的影子,从沙箱有两个 入口可以看出来(一个是proxySandbox.ts,另一个是snapshotSandbox.ts)

简单总结下其实现思路:起初版本使用了快照沙箱的概念,模拟ES6 的 Proxy API,通过代理劫持 window ,当子应用修改或使用window上的属性或方法时,把对应的操作记录下来,每次子应用挂载/卸载时生成快照,当再次从外部切换到当前子应用时,再从记录的快照中恢复,而后来为了兼容多个子应用共存的情况,又基于Proxy实现了代理所有全局性的常量和方法接口,为每个子应用构造了独立的运行环境。

方案2:阿里云开发平台的 Browser VM借助iframe的方案

原理就是接触iframe生成window,将子应用的包装成一个闭包方法,将iframe生成的window传入,借助iframe天生的优势(硬隔离),非常nice的解决了各个子应用的隔离,也不需要自行维护window这种情况,但缺点也很明显,所有执行必须通过postMessage

与主线程通信,易用性以及 postMessage 序列化带来的性能等问题都深深困扰的开发者。

方案3:Figma 采用的方案

基于目前还在草案阶段 Realm API,并将 JavaScript 解释器的一种 C++ 实现 Duktape 编译到了 WebAssembly,然后将其嵌入到Realm上下文中,实现了其产品下的三方插件的独立运行

这种方案和探索的基于 WebWorker的实现可能能够结合得更好,值得注意的是,Realm 同样可以使用 JavaScript 目前已有的特性来实现,即 with与Proxy。这也是目前社区比较流行的沙箱方案。

示例

快照沙箱

snapshotSandbox会污染全局window,但是可以支持不兼容Proxy的浏览器。

javaScript 复制代码
/*
      基于diff来实现的沙箱,用于不支持window.Proxy低版本浏览器, 快照沙箱
    */
const iter = (window, callback) => {
  for (const prop in window) {
    if (window.hasOwnProperty(prop)) {
      callback(prop);
    }
  }
};
class SnapshotSandbox {
  constructor() {
    this.proxy = window;
    this.modifyPropsMap = {};
  }
  // 激活沙箱时,将window的快照信息存到windowSnapshot中
  active() {
    // 缓存active状态的window
    this.windowSnapshot = {};
    // 如果modifyPropsMap有值,还需要还原上次的状态;激活期间,可能修改了window的数据;
    iter(window, (prop) => {
      this.windowSnapshot[prop] = window[prop];
    });
    Object.keys(this.modifyPropsMap).forEach((p) => {
      window[p] = this.modifyPropsMap[p];
    });
  }
  // 退出沙箱时,将修改过的信息存到modifyPropsMap里面,并且把window还原成初始进入的状态。
  inactive() {
    iter(window, (prop) => {
      if (this.windowSnapshot[prop] !== window[prop]) {
        // 记录变更
        this.modifyPropsMap[prop] = window[prop];
        // 还原window
        window[prop] = this.windowSnapshot[prop];
      }
    });
  }
}
// 进来的时候,记录一下当前window的属性
// 退出的时候,记录修改,并且把window还原到进入的时候
// 下次进入的时候,把修改属性放到window上
const sandbox = new SnapshotSandbox();
((window) => {
  // 激活沙箱
  sandbox.active();
  window.sex = "男";
  window.age = "22";
  console.log(window.sex, window.age);
  // 失活沙箱
  sandbox.inactive();
  console.log(window.sex, window.age);
  // 激活沙箱
  sandbox.active();
  console.log(window.sex, window.age);
})(sandbox.proxy);

代理沙箱

qiankun基于es6的Proxy实现了两种应用场景不同的沙箱,一种是legacySandbox(单例),一种是proxySandbox(多例)。因为都是基于Proxy实现的,所以都称为代理沙箱。

以多例 proxySandbox为例

只有proxySandbox才是对window进行了一个真正的无污染环境

总结

JavaScript 沙箱隔离在社区是个经久不衰的话题,尤其是19年后,随着微前端而变的有所发展,最简单的 iframe 标签 Sandbox 属性就已经能做到 JavaScript 运行时的隔离,社区较为流行的是利用一些语言特性(with、realm、Proxy 等 API )屏蔽(或代理) Window、Document 等全局对象,建立白名单机制,对可能潜在危险操作的 API 重写(如阿里云 Console OS - Browser VM),另一种就是更高端的操作了,就好像Figma一样,使用这种尝试嵌入平台无关的 JavaScript 解释器,所有第三方代码都通过嵌入的解释器来执行。

相关推荐
前进的搬砖er8 分钟前
4.three.js的几何体、纹理、光源、材质、uv属性 讲义
前端
前端毕业班22 分钟前
uni-app 小程序支持 teleport 了
前端·javascript·vue.js
Fabarta25 分钟前
从 0 实现 ChatGPT 风格的流式对话 UI
算法·架构
东风破_1 小时前
LLM 为什么记不住你?无状态、messages 和上下文管理
javascript·人工智能
梨子同志1 小时前
qiankun
前端
梨子同志1 小时前
Nginx
前端
梨子同志1 小时前
NestJS
前端
梨子同志1 小时前
Next.js
前端
搭贝1 小时前
低代码平台私有化部署架构与信创全栈适配实战
低代码·架构·rxjava
谭光志2 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程