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 相关文章推荐
javascript之ESC(第二类混淆)
May 06 Javascript
JQuery 学习笔记 选择器之三
Jul 23 Javascript
js获取当前页面的url网址信息
Jun 12 Javascript
JavaScript在Android的WebView中parseInt函数转换不正确问题解决方法
Apr 25 Javascript
浅谈jQuery的bind和unbind事件(绑定和解绑事件)
Mar 02 Javascript
详解AngularJS 模块化
Jun 14 Javascript
在vue中使用SockJS实现webSocket通信的过程
Aug 29 Javascript
探秘vue-rx 2.0(推荐)
Sep 21 Javascript
javascript实现切割轮播效果
Nov 28 Javascript
ant design实现圈选功能
Dec 17 Javascript
如何在vue中使用video.js播放m3u8格式的视频
Feb 01 Vue.js
vue 使用 v-model 双向绑定父子组件的值遇见的问题及解决方案
Mar 01 Vue.js
quickjs 封装 JavaScript 沙箱详情
Nov 02 #Javascript
js 数组 fill() 填充方法
浅谈 JavaScript 沙箱Sandbox
详解 TypeScript 枚举类型
Nov 02 #Javascript
前端JavaScript大管家 package.json
JavaScript 原型与原型链详情
javascript实现计算器功能详解流程
You might like
thinkphp中空模板与空模块的用法实例
2014/11/26 PHP
php结合redis高并发下发帖、发微博的实现方法
2016/12/15 PHP
apycom出品的jQuery精美菜单破解方法
2011/02/18 Javascript
让js弹出窗口居前显示的实现方法
2013/07/10 Javascript
jQuery 删除/替换DOM元素的几种方式
2014/05/20 Javascript
ECMAScript6的新特性箭头函数(Arrow Function)详细介绍
2014/06/07 Javascript
JavaScript基础函数整理汇总
2015/01/30 Javascript
JS实用的动画弹出层效果实例
2015/05/05 Javascript
javascript工厂模式和构造函数模式创建对象方法解析
2016/12/30 Javascript
基于Bootstrap模态对话框只加载一次 remote 数据的解决方法
2017/07/09 Javascript
Vue组件和Route的生命周期实例详解
2018/02/10 Javascript
js屏蔽退格键(backspace或者叫后退键与F5)
2019/02/10 Javascript
JavaScript中reduce()的5个基本用法示例
2020/07/19 Javascript
[02:05]DOTA2完美大师赛趣味视频之看我表演
2017/11/18 DOTA
[48:31]DOTA2-DPC中国联赛 正赛 Dynasty vs XG BO3 第一场 2月2日
2021/03/11 DOTA
用Python的Django框架编写从Google Adsense中获得报表的应用
2015/04/17 Python
由Python运算π的值深入Python中科学计算的实现
2015/04/17 Python
用Python编写web API的教程
2015/04/30 Python
Python中getattr函数和hasattr函数作用详解
2016/06/14 Python
python3+PyQt5使用数据库表视图
2018/04/24 Python
python 统计列表中不同元素的数量方法
2018/06/29 Python
详解Python3中ceil()函数用法
2019/02/19 Python
浅谈python的深浅拷贝以及fromkeys的用法
2019/03/08 Python
python写日志文件操作类与应用示例
2019/07/01 Python
django 数据库 get_or_create函数返回值是tuple的问题
2020/05/15 Python
Python无损压缩图片的示例代码
2020/08/06 Python
HTML5页面直接调用百度地图API获取当前位置直接导航目的地的实现代码
2018/03/02 HTML / CSS
使用HTML和CSS实现的标签云效果(附demo)
2021/02/03 HTML / CSS
秋季运动会加油稿200字
2014/01/11 职场文书
学校卫生检查制度
2014/02/03 职场文书
财务会计专业求职信
2014/06/09 职场文书
党员学习中共十八大思想报告
2014/09/12 职场文书
大学生暑期实践报告之企业经营管理
2019/08/08 职场文书
十大最强妖精系宝可梦,哲尔尼亚斯实力最强,第五被称为大力士
2022/03/18 日漫
Netflix《海贼王》真人版剧集多张片场照曝光
2022/04/04 日漫
oracle设置密码复杂度及设置超时退出的功能
2022/06/28 Oracle