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实现的仿新浪微博完美的时间组件升级版
Dec 20 Javascript
DWR实现模拟Google搜索效果实现原理及代码
Jan 30 Javascript
js数组的操作指南
Dec 28 Javascript
常见JS验证脚本汇总
Dec 01 Javascript
js实现滚动条滚动到某个位置便自动定位某个tr
Jan 20 Javascript
基于BootStrap Metronic开发框架经验小结【一】框架总览及菜单模块的处理
May 12 Javascript
js中new一个对象的过程
Feb 20 Javascript
JS实现针对给定时间的倒计时功能示例
Apr 11 Javascript
浅谈js使用in和hasOwnProperty获取对象属性的区别
Apr 27 Javascript
基于jQuery的表单填充实例
Aug 22 jQuery
vue-router项目实战总结篇
Feb 11 Javascript
vue如何使用async、await实现同步请求
Dec 09 Javascript
quickjs 封装 JavaScript 沙箱详情
Nov 02 #Javascript
js 数组 fill() 填充方法
浅谈 JavaScript 沙箱Sandbox
详解 TypeScript 枚举类型
Nov 02 #Javascript
前端JavaScript大管家 package.json
JavaScript 原型与原型链详情
javascript实现计算器功能详解流程
You might like
ThinkPHP3.1新特性之对分组支持的改进与完善概述
2014/06/19 PHP
PHP比你想象的好得多
2014/11/27 PHP
php+ajax 实现输入读取数据库显示匹配信息
2015/10/08 PHP
PHP session会话操作技巧小结
2016/09/27 PHP
PHP批量修改文件名称的方法分析
2017/02/27 PHP
Add a Formatted Table to a Word Document
2007/06/15 Javascript
Javascript 面向对象 重载
2010/05/13 Javascript
jQuery cdn使用介绍
2013/05/08 Javascript
JS幻灯片可循环播放可平滑旋转带滚动导航(自写)
2013/08/05 Javascript
jquery统计输入文字的个数并对其进行判断
2014/01/07 Javascript
jquery实现弹出层遮罩效果的简单实例
2014/03/03 Javascript
项目中常用的JS方法整理
2015/01/30 Javascript
基于jQuery插件实现环形图标菜单旋转切换特效
2015/05/15 Javascript
JavaScript实现的Tween算法及缓冲特效实例代码
2015/11/03 Javascript
学习JavaScript设计模式之单例模式
2016/01/19 Javascript
jQuery点击其他地方时菜单消失的实现方法
2016/04/22 Javascript
js 判断一组日期是否是连续的简单实例
2016/07/11 Javascript
layui框架中layer父子页面交互的方法分析
2017/11/15 Javascript
详解vue-cli 构建项目 vue-cli请求后台接口 vue-cli使用axios、sass、swiper
2018/05/28 Javascript
jQuery动态移除与增加onclick属性的方法详解
2018/06/07 jQuery
JavaScript字符串转数字的5种方法及遇到的坑
2018/07/16 Javascript
javascript自定义日期比较函数用法示例
2019/07/22 Javascript
layui 监听select选择 获取当前select的ID名称方法
2019/09/24 Javascript
解决vue页面刷新,数据丢失的问题
2020/11/24 Vue.js
[00:17]DOTA2荣耀之路5:It’s a disastah!
2018/05/28 DOTA
[03:21]【TI9纪实】Old Boys
2019/08/23 DOTA
Python和Perl绘制中国北京跑步地图的方法
2016/03/03 Python
Python进程,多进程,获取进程id,给子进程传递参数操作示例
2019/10/11 Python
Anaconda的安装与虚拟环境建立
2020/11/18 Python
AVON雅芳官网:世界上最大的美容化妆品公司之一
2016/11/02 全球购物
货车司机岗位职责
2014/03/18 职场文书
2015年三万活动总结
2015/03/25 职场文书
安全教育培训心得体会
2016/01/15 职场文书
小程序wx.getUserProfile接口的具体使用
2021/06/02 Javascript
Golang日志包的使用
2022/04/20 Golang
Spring boot admin 服务监控利器详解
2022/08/05 Java/Android