您现在的位置是:亿华云 > IT科技
基于 WebWorker 封装 JavaScript 沙箱
亿华云2025-10-08 21:01:47【IT科技】5人已围观
简介在前文基于quickjs 封装 JavaScript 沙箱已经基于 quickjs 实现了一个沙箱,这里再基于 web worker 实现备用方案。如果你不知道 web worker 是什么或者从未了
在前文 基于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 直接支持消息通信的云南idc服务商 api很赞哦!(3461)