WebWorker 封装 JavaScript 沙箱详情


Posted in Javascript onNovember 02, 2021

1、场景

在前文  quickjs 封装 JavaScript 沙箱详情 已经基于 quickjs 实现了一个沙箱,这里再基于 web worker 实现备用方案。如果你不知道 web worker 是什么或者从未了解过,可以查看 Web Workers API。简而言之,它是一个浏览器实现的多线程,可以运行一段代码在另一个线程,并且提供与之通信的功能。

2、实现 IJavaScriptShadowbox

事实上,web worker 提供了 event emitter 的 api,即 postMessage/onmessage,所以实现非常简单。

实现分为两部分,一部分是在主线程实现 IJavaScriptShadowbox,另一部分则是需要在 web worker 线程实现 IEventEmitter

2.1 主线程的实现

import { IJavaScriptShadowbox } from "./IJavaScriptShadowbox";

export class WebWorkerShadowbox implements IJavaScriptShadowbox {
  destroy(): void {
    this.worker.terminate();
  }

  private worker!: Worker;
  eval(code: string): void {
    const blob = new Blob([code], { type: "application/javascript" });
    this.worker = new Worker(URL.createObjectURL(blob), {
      credentials: "include",
    });
    this.worker.addEventListener("message", (ev) => {
      const msg = ev.data as { channel: string; data: any };
      // console.log('msg.data: ', msg)
      if (!this.listenerMap.has(msg.channel)) {
        return;
      }
      this.listenerMap.get(msg.channel)!.forEach((handle) => {
        handle(msg.data);
      });
    });
  }

  private readonly listenerMap = new Map<string, ((data: any) => void)[]>();
  emit(channel: string, data: any): void {
    this.worker.postMessage({
      channel: channel,
      data,
    });
  }
  on(channel: string, handle: (data: any) => void): void {
    if (!this.listenerMap.has(channel)) {
      this.listenerMap.set(channel, []);
    }
    this.listenerMap.get(channel)!.push(handle);
  }
  offByChannel(channel: string): void {
    this.listenerMap.delete(channel);
  }
}

2.2 web worker 线程的实现

import { IEventEmitter } from "./IEventEmitter";

export class WebWorkerEventEmitter implements IEventEmitter {
  private readonly listenerMap = new Map<string, ((data: any) => void)[]>();

  emit(channel: string, data: any): void {
    postMessage({
      channel: channel,
      data,
    });
  }

  on(channel: string, handle: (data: any) => void): void {
    if (!this.listenerMap.has(channel)) {
      this.listenerMap.set(channel, []);
    }
    this.listenerMap.get(channel)!.push(handle);
  }

  offByChannel(channel: string): void {
    this.listenerMap.delete(channel);
  }

  init() {
    onmessage = (ev) => {
      const msg = ev.data as { channel: string; data: any };
      if (!this.listenerMap.has(msg.channel)) {
        return;
      }
      this.listenerMap.get(msg.channel)!.forEach((handle) => {
        handle(msg.data);
      });
    };
  }

  destroy() {
    this.listenerMap.clear();
    onmessage = null;
  }
}

3、使用 WebWorkerShadowbox/WebWorkerEventEmitter

主线程代码

const shadowbox: IJavaScriptShadowbox = new WebWorkerShadowbox();
shadowbox.on("hello", (name: string) => {
  console.log(`hello ${name}`);
});
// 这里的 code 指的是下面 web worker 线程的代码
shadowbox.eval(code);
shadowbox.emit("open");

web worker 线程代码

const em = new WebWorkerEventEmitter();
em.on("open", () => em.emit("hello", "liuli"));

下面是代码的执行流程示意图;web worker 沙箱实现使用示例代码的执行流程:

WebWorker 封装 JavaScript 沙箱详情

4、限制 web worker 全局 api

经大佬 JackWoeker 提醒,web worker 有许多不安全的 api,所以必须限制,包含但不限于以下 api

  • fetch
  • indexedDB
  • performance

事实上,web worker 默认自带了 276 个全局 api,可能比我们想象中多很多。

WebWorker 封装 JavaScript 沙箱详情

有篇 文章 阐述了如何在 web 上通过 performance/SharedArrayBuffer api 做侧信道攻击,即便现在 SharedArrayBuffer api 现在浏览器默认已经禁用了,但天知道还有没有其他方法。所以最安全的方法是设置一个 api 白名单,然后删除掉非白名单的 api。

// whitelistWorkerGlobalScope.ts
/**
 * 设定 web worker 运行时白名单,ban 掉所有不安全的 api
 */
export function whitelistWorkerGlobalScope(list: PropertyKey[]) {
  const whitelist = new Set(list);
  const all = Reflect.ownKeys(globalThis);
  all.forEach((k) => {
    if (whitelist.has(k)) {
      return;
    }
    if (k === "window") {
      console.log("window: ", k);
    }
    Reflect.deleteProperty(globalThis, k);
  });
}

/**
 * 全局值的白名单
 */
const whitelist: (
  | keyof typeof global
  | keyof WindowOrWorkerGlobalScope
  | "console"
)[] = [
  "globalThis",
  "console",
  "setTimeout",
  "clearTimeout",
  "setInterval",
  "clearInterval",
  "postMessage",
  "onmessage",
  "Reflect",
  "Array",
  "Map",
  "Set",
  "Function",
  "Object",
  "Boolean",
  "String",
  "Number",
  "Math",
  "Date",
  "JSON",
];

whitelistWorkerGlobalScope(whitelist);

然后在执行第三方代码前先执行上面的代码

import beforeCode from "./whitelistWorkerGlobalScope.js?raw";

export class WebWorkerShadowbox implements IJavaScriptShadowbox {
  destroy(): void {
    this.worker.terminate();
  }

  private worker!: Worker;
  eval(code: string): void {
    // 这行是关键
    const blob = new Blob([beforeCode + "\n" + code], {
      type: "application/javascript",
    });
    // 其他代码。。。
  }
}

由于我们使用 ts 编写源码,所以还必须将 ts 打包为 js bundle,然后通过 vite 的 ?raw 作为字符串引入,下面吾辈写了一个简单的插件来完成这件事。

import { defineConfig, Plugin } from "vite";
import reactRefresh from "@vitejs/plugin-react-refresh";
import checker from "vite-plugin-checker";
import { build } from "esbuild";
import * as path from "path";

export function buildScript(scriptList: string[]): Plugin {
  const _scriptList = scriptList.map((src) => path.resolve(src));
  async function buildScript(src: string) {
    await build({
      entryPoints: [src],
      outfile: src.slice(0, src.length - 2) + "js",
      format: "iife",
      bundle: true,
      platform: "browser",
      sourcemap: "inline",
      allowOverwrite: true,
    });
    console.log("构建完成: ", path.relative(path.resolve(), src));
  }
  return {
    name: "vite-plugin-build-script",

    async configureServer(server) {
      server.watcher.add(_scriptList);
      const scriptSet = new Set(_scriptList);
      server.watcher.on("change", (filePath) => {
        // console.log('change: ', filePath)
        if (scriptSet.has(filePath)) {
          buildScript(filePath);
        }
      });
    },
    async buildStart() {
      // console.log('buildStart: ', this.meta.watchMode)
      if (this.meta.watchMode) {
        _scriptList.forEach((src) => this.addWatchFile(src));
      }
      await Promise.all(_scriptList.map(buildScript));
    },
  };
}

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    reactRefresh(),
    checker({ typescript: true }),
    buildScript([path.resolve("src/utils/app/whitelistWorkerGlobalScope.ts")]),
  ],
});

现在,我们可以看到 web worker 中的全局 api 只有白名单中的那些了。

WebWorker 封装 JavaScript 沙箱详情

5、web worker 沙箱的主要优势

可以直接使用 chrome devtool 调试
直接支持 console/setTimeout/setInterval api
直接支持消息通信的 api

到此这篇关于WebWorker 封装 JavaScript 沙箱详情的文章就介绍到这了,更多相关WebWorker 封装 JavaScript 沙箱内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
脚本吧 - 幻宇工作室用到js,超强推荐expand.js
Dec 23 Javascript
JavaScript效率调优经验
Jun 04 Javascript
Jquery实战_读书笔记2 选择器
Jan 22 Javascript
Jquery通过Ajax方式来提交Form表单的具体实现
Nov 07 Javascript
JQuery日历插件My97DatePicker日期范围限制
Jan 20 Javascript
jQuery实现遍历复选框的方法示例
Mar 06 Javascript
CSS3+JavaScript实现翻页幻灯片效果
Jun 28 Javascript
zTree树形菜单交互选项卡效果的实现方法
Dec 25 Javascript
JavaScript设计模式之享元模式实例详解
Jan 17 Javascript
JS中的一些常用的函数式编程术语
Jun 15 Javascript
基于纯JS实现多张图片的懒加载Lazy过程解析
Oct 14 Javascript
Vue router安装及使用方法解析
Dec 02 Vue.js
quickjs 封装 JavaScript 沙箱详情
Nov 02 #Javascript
js 数组 fill() 填充方法
浅谈 JavaScript 沙箱Sandbox
详解 TypeScript 枚举类型
Nov 02 #Javascript
前端JavaScript大管家 package.json
JavaScript 原型与原型链详情
javascript实现计算器功能详解流程
You might like
PHP的面向对象编程
2006/10/09 PHP
PHP常用技术文之文件操作和目录操作总结
2014/09/27 PHP
PHP实现在线阅读PDF文件的方法
2015/06/23 PHP
php封装的page分页类完整实例代码
2020/02/01 PHP
改版了网上的一个js操作userdata
2007/04/27 Javascript
js跟随滚动条滚动浮动代码
2009/12/31 Javascript
jQuery结合Json提交数据到Webservice,并接收从Webservice返回的Json数据
2011/02/18 Javascript
JS教程:window.location使用方法的区别介绍
2013/10/04 Javascript
js对象转json数组的简单实现案例
2014/02/28 Javascript
Javascript中的Array数组对象详谈
2014/03/03 Javascript
javascript在当前窗口关闭前检测窗口是否关闭
2014/09/29 Javascript
JavaScript实现点击自动选择TextArea文本的方法
2015/07/02 Javascript
javascript伸缩型菜单实现代码
2015/11/16 Javascript
原生js实现图片轮播特效
2015/12/18 Javascript
信息页文内画中画广告js实现代码(文中加载广告方式)
2016/01/03 Javascript
jQuery遍历DOM节点操作之filter()方法详解
2016/04/14 Javascript
JavaScript lodash常见用法系列小结
2016/08/24 Javascript
判断颜色是否合法的正则表达式(详解)
2017/05/03 Javascript
javascript 中select框触发事件过程的分析
2017/08/01 Javascript
从parcel.js打包出错到选择nvm的全部过程
2018/01/23 Javascript
javascript实现手动点赞效果
2019/04/09 Javascript
JQuery使用属性addClass、removeClass和toggleClass实现增加和删除类操作示例
2019/11/18 jQuery
原生javascript制作贪吃蛇小游戏的方法分析
2020/02/26 Javascript
JSONP 的原理、理解 与 实例分析
2020/05/16 Javascript
[01:29]2017 DOTA2国际邀请赛官方英雄手办展示
2017/03/18 DOTA
python实现比较类的两个instance(对象)是否相等的方法分析
2019/06/26 Python
Python SMTP配置参数并发送邮件
2020/06/16 Python
采用冷却技术的超自然舒适度:GhostBed床垫
2018/09/18 全球购物
好的演讲稿开场白
2013/12/30 职场文书
培训班主持词
2014/03/28 职场文书
商场客服专员岗位职责
2014/06/13 职场文书
医生个人年度总结
2015/02/28 职场文书
余世维讲座观后感
2015/06/11 职场文书
人民的好儿女观后感
2015/06/18 职场文书
2015年第31个教师节致辞
2015/07/31 职场文书
小学数学教师研修感悟
2015/11/18 职场文书