WebWorker 封裝 JavaScript 沙箱詳情
1、場景
在前文 quickjs 封裝 JavaScript 沙箱詳情 已經(jīng)基于 quickjs 實(shí)現(xiàn)了一個沙箱,這里再基于 web worker 實(shí)現(xiàn)備用方案。如果你不知道 web worker 是什么或者從未了解過,可以查看 Web Workers API。簡而言之,它是一個瀏覽器實(shí)現(xiàn)的多線程,可以運(yùn)行一段代碼在另一個線程,并且提供與之通信的功能。
2、實(shí)現(xiàn) IJavaScriptShadowbox
事實(shí)上,web worker 提供了 event emitter 的 api,即 postMessage/onmessage,所以實(shí)現(xiàn)非常簡單。
實(shí)現(xiàn)分為兩部分,一部分是在主線程實(shí)現(xiàn) IJavaScriptShadowbox,另一部分則是需要在 web worker 線程實(shí)現(xiàn) IEventEmitter
2.1 主線程的實(shí)現(xiàn)
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 線程的實(shí)現(xiàn)
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"));
下面是代碼的執(zhí)行流程示意圖;web worker 沙箱實(shí)現(xiàn)使用示例代碼的執(zhí)行流程:

4、限制 web worker 全局 api
經(jīng)大佬 JackWoeker 提醒,web worker 有許多不安全的 api,所以必須限制,包含但不限于以下 api
fetch
indexedDB
performance
事實(shí)上,web worker 默認(rèn)自帶了 276 個全局 api,可能比我們想象中多很多。

有篇 文章 闡述了如何在 web 上通過 performance/SharedArrayBuffer api 做側(cè)信道攻擊,即便現(xiàn)在 SharedArrayBuffer api 現(xiàn)在瀏覽器默認(rèn)已經(jīng)禁用了,但天知道還有沒有其他方法。所以最安全的方法是設(shè)置一個 api 白名單,然后刪除掉非白名單的 api。
// whitelistWorkerGlobalScope.ts
/**
* 設(shè)定 web worker 運(yùn)行時白名單,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);
然后在執(zhí)行第三方代碼前先執(zhí)行上面的代碼
import beforeCode from "./whitelistWorkerGlobalScope.js?raw";
export class WebWorkerShadowbox implements IJavaScriptShadowbox {
destroy(): void {
this.worker.terminate();
}
private worker!: Worker;
eval(code: string): void {
// 這行是關(guān)鍵
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("構(gòu)建完成: ", 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")]),
],
});
現(xiàn)在,我們可以看到 web worker 中的全局 api 只有白名單中的那些了。

5、web worker 沙箱的主要優(yōu)勢
可以直接使用 chrome devtool 調(diào)試
直接支持 console/setTimeout/setInterval api直接支持消息通信的
api
到此這篇關(guān)于WebWorker 封裝 JavaScript 沙箱詳情的文章就介紹到這了,更多相關(guān)WebWorker 封裝 JavaScript 沙箱內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
微信小程序 獲取session_key和openid的實(shí)例
這篇文章主要介紹了微信小程序 獲取session_key和openid的實(shí)例的相關(guān)資料,希望能通過本文幫助到大家實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-08-08
ComponentLoader?與動態(tài)組件實(shí)例詳解
這篇文章主要為大家介紹了ComponentLoader?與動態(tài)組件實(shí)現(xiàn)實(shí)例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05
JS前端并發(fā)多個相同的請求控制為只發(fā)一個請求方式
這篇文章主要為大家介紹了JS前端并發(fā)多個相同的請求控制為只發(fā)一個請求方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07

