您当前的位置:首页 > 网站建设 > javascript
| php | asp | css | H5 | javascript | Mysql | Dreamweaver | Delphi | 网站维护 | 帝国cms | React | 考试系统 | ajax | jQuery | 小程序 |

WebWorker 封装 JavaScript 沙箱详情

51自学网 2022-02-21 13:38:40
  javascript

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 沙箱实现使用示例代码的执行流程:

4、限制 web worker 全局 api

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

  • fetch
  • indexedDB
  • performance

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

有篇 文章 阐述了如何在 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 只有白名单中的那些了。

5、web worker 沙箱的主要优势

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

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


下载地址:
quickjs 封装 JavaScript 沙箱详情
浅谈克隆 JavaScript
万事OK自学网:51自学网_软件自学网_CAD自学网自学excel、自学PS、自学CAD、自学C语言、自学css3实例,是一个通过网络自主学习工作技能的自学平台,网友喜欢的软件自学网站。