深入理解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的浏览器。

深入理解JS沙箱

/*
      基于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进行了一个真正的无污染环境

深入理解JS沙箱

总结

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

原文链接:https://juejin.cn/post/7265995654564233277 作者:十七喜欢前端

(0)
上一篇 2023年8月13日 上午10:26
下一篇 2023年8月13日 上午10:37

相关推荐

发表回复

登录后才能评论