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

mysql 8.0.15 winx64 decompression version graphic installation tutorial

Every time after installing the system, I have to...

How to insert video into HTML and make it compatible with all browsers

There are two most commonly used methods to insert...

Docker installation tutorial in Linux environment

1. Installation environment Docker supports the f...

Discussion on the problem of iframe node initialization

Today I suddenly thought of reviewing the producti...

MySQL and MySQL Workbench Installation Tutorial under Ubuntu

Ubuntu install jdk: [link] Install Eclipse on Ubu...

Detailed explanation of tcpdump command examples in Linux

Preface To put it simply, tcpdump is a packet ana...

A brief summary of my experience in writing HTML pages

It has been three or four months since I joined Wo...

Steps to use ORM to add data in MySQL

【Foreword】 If you want to use ORM to operate data...

Installation and use of mysql on Ubuntu (general version)

Regardless of which version of Ubuntu, installing...

Setting up shadowsocks+polipo global proxy in Linux environment

1. Install shadowsocks sudo apt-get install pytho...

Tomcat uses Log4j to output catalina.out log

Tomcat's default log uses java.util.logging, ...

How to query date and time in mysql

Preface: In project development, some business ta...

MySQL database Shell import_table data import

Table of contents MySQL Shell import_table data i...

Common methods and problems of Docker cleaning

If you use docker for large-scale development but...