基于 WebWorker 封装 JavaScript 沙箱

域名2025-11-04 19:21:47342

  

在前文 基于quickjs 封装 JavaScript 沙箱 已经基于 quickjs 实现了一个沙箱,基于这里再基于 web worker 实现备用方案。基于如果你不知道 web worker 是基于什么或者从未了解过,可以查看 Web Workers API 。基于简而言之,基于它是基于一个浏览器实现的多线程,可以运行一段代码在另一个线程,基于并且提供与之通信的基于功能。

实现 IJavaScriptShadowbox

事实上,基于web worker 提供了 event emitter 的基于 api,即 postMessage/onmessage ,基于所以实现非常简单。基于

实现分为两部分,基于一部分是基于在主线程实现 IJavaScriptShadowbox ,另一部分则是基于需要在 web worker 线程实现 IEventEmitter

主线程的服务器托管实现

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);   } } 

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;   } } 

使用

主线程代码

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 全局 api

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

fetch indexedDB performance

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

Snipaste_2021-10-24_23-05-18

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

1635097498575

web worker 沙箱的主要优势

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

本文地址:http://www.bzve.cn/html/638d65698705.html
版权声明

本文仅代表作者观点,不代表本站立场。
本文系作者授权发表,未经许可,不得转载。

全站热门

大白菜GPT分区教程(详解大白菜GPT分区教程,帮助您轻松完成硬盘分区操作)

Vsftpd如何匿名访问

嚣张!黑客袭击国际刑事法院

Ubuntu server上搭建LAMP:安装apache和mysql

如何更换旧电脑的网卡(简单易懂的教程,轻松升级你的网络连接)

MySQL数据库的优化(上)单机MySQL数据库的优化

Flink SQL 知其所以然:基础 DML SQL 执行语义!

总结出这套数据库迁移经验,我花了20年……

友情链接

滇ICP备2023006006号-39