WebWorker encapsulates JavaScript sandbox details

WebWorker encapsulates JavaScript sandbox details

1. Scenario

In the previous article, Quickjs encapsulates JavaScript sandbox details, a sandbox has been implemented based on quickjs . Here, an alternative solution is implemented based on web workers. If you don't know what web worker is or have never looked into it, check out Web Workers API . In short, it is a browser-implemented multithreading that can run a piece of code in another thread and provide the ability to communicate with it.

2. Implement IJavaScriptShadowbox

In fact, web worker provides an event emitter API, namely postMessage/onmessage , so the implementation is very simple.

The implementation is divided into two parts, one is to implement IJavaScriptShadowbox in the main thread, and the other is to implement IEventEmitter in the web worker thread.

2.1 Implementation of the main thread

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 Implementation of web worker threads

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. Use WebWorkerShadowbox/WebWorkerEventEmitter

Main thread code

const shadowbox: IJavaScriptShadowbox = new WebWorkerShadowbox();
shadowbox.on("hello", (name: string) => {
  console.log(`hello ${name}`);
});
// The code here refers to the code of the web worker thread below shadowbox.eval(code);
shadowbox.emit("open");


Web worker thread code

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


The following is a schematic diagram of the code execution flow; web worker sandbox implementation uses the sample code execution flow:

4. Limit web worker global API

As JackWoeker reminded, web worker have many unsafe APIs, so they must be restricted, including but not limited to the following APIs

  • fetch
  • indexedDB
  • performance

In fact, web worker come with 276 global APIs by default, which may be much more than we think.

There is an article that explains how to perform side-channel attacks on the web through performance/SharedArrayBuffer api . Even though在SharedArrayBuffer api is now disabled by default in browsers, who knows if there are other ways. So the safest way is to set up an API whitelist and then delete the non-whitelisted APIs.

// whitelistWorkerGlobalScope.ts
/**
 * Set the web worker runtime whitelist to ban all unsafe APIs
 */
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);
  });
}

/**
 * Whitelist of global values ​​*/
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);

Then execute the above code before executing the third-party code

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

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

  private worker!: Worker;
  eval(code: string): void {
    // This line is the key const blob = new Blob([beforeCode + "\n" + code], {
      type: "application/javascript",
    });
    // Other code. . .
  }
}

Since we use ts to write source code, we must also package ts into js bundle and then import it as a string through vite 's ? raw . Below we wrote a simple plugin to do this.

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("Build completed: ", 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")]),
  ],
});

Now, we can see that the global APIs in web worker are only those in the whitelist.

5. The main advantages of web worker sandbox

You can use chrome devtool to debug directly and support console/setTimeout/setInterval api
api that directly supports message communication

This is the end of this article about the details of WebWorker encapsulating JavaScript sandbox. For more related content about WebWorker encapsulating JavaScript sandbox, please search for previous articles on 123WORDPRESS.COM or continue to browse the related articles below. I hope everyone will support 123WORDPRESS.COM in the future!

You may also be interested in:
  • Quickjs encapsulates JavaScript sandbox details
  • JavaScript Sandbox Exploration
  • A brief talk about JavaScript Sandbox
  • A brief discussion on several ways to implement front-end JS sandbox
  • A brief discussion on Node.js sandbox environment
  • Setting up a secure sandbox environment for Node.js applications
  • Example of sandbox mode in JS implementation closure
  • JS sandbox mode example analysis
  • JavaScript design pattern security sandbox mode

<<:  Understand CSS3 Grid layout in 10 minutes

>>:  Why MySQL does not recommend using subqueries and joins

Recommend

CSS sets Overflow to hide the scroll bar while allowing scrolling

CSS sets Overflow to hide the scroll bar while al...

JavaScript implements circular carousel

This article shares the specific code of JavaScri...

Linux common basic commands and usage

This article uses examples to illustrate common b...

JS implements array filtering from simple to multi-condition filtering

Table of contents Single condition single data fi...

MySQL 8.0.18 installation and configuration graphic tutorial

Learning objectives: Learn to use Windows system ...

Detailed explanation of this pointing problem in JavaScript function

this keyword Which object calls the function, and...

Solution - BASH: /HOME/JAVA/JDK1.8.0_221/BIN/JAVA: Insufficient permissions

1) Enter the folder path where the jdk file is st...

jQuery realizes the shuttle box function

This article example shares the specific code of ...

React+Amap obtains latitude and longitude in real time and locates the address

Table of contents 1. Initialize the map 2. Map Po...

Vue development tree structure components (component recursion)

This article example shares the specific code of ...

How to install Solr 8.6.2 in Docker and configure the Chinese word segmenter

1. Environment version Docker version 19.03.12 ce...

How to use JavaScript to determine several common browsers through userAgent

Preface Usually when making h5 pages, you need to...

Detailed explanation of MySQL backup and recovery practice of mysqlbackup

1. Introduction to mysqlbackup mysqlbackup is the...

Detailed installation history of Ubuntu 20.04 LTS

This article records the creation of a USB boot d...