WebWorker 封裝 JavaScript 沙箱詳情
1、場(chǎng)景
在前文 quickjs 封裝 JavaScript 沙箱詳情 已經(jīng)基于 quickjs
實(shí)現(xiàn)了一個(gè)沙箱,這里再基于 web worker 實(shí)現(xiàn)備用方案。如果你不知道 web worker
是什么或者從未了解過(guò),可以查看 Web Workers API
。簡(jiǎn)而言之,它是一個(gè)瀏覽器實(shí)現(xiàn)的多線程,可以運(yùn)行一段代碼在另一個(gè)線程,并且提供與之通信的功能。
2、實(shí)現(xiàn) IJavaScriptShadowbox
事實(shí)上,web worker 提供了 event emitter
的 api,即 postMessage/onmessage
,所以實(shí)現(xiàn)非常簡(jiǎ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
個(gè)全局 api,可能比我們想象中多很多。
有篇 文章 闡述了如何在 web 上通過(guò) performance/SharedArrayBuffer api
做側(cè)信道攻擊,即便現(xiàn)在 SharedArrayBuffer api
現(xiàn)在瀏覽器默認(rèn)已經(jīng)禁用了,但天知道還有沒(méi)有其他方法。所以最安全的方法是設(shè)置一個(gè) api 白名單,然后刪除掉非白名單的 api。
// whitelistWorkerGlobalScope.ts /** * 設(shè)定 web worker 運(yùn)行時(shí)白名單,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 編寫(xiě)源碼,所以還必須將 ts 打包為 js bundle
,然后通過(guò) vite
的 ?raw
作為字符串引入,下面吾輩寫(xiě)了一個(gè)簡(jiǎn)單的插件來(lái)完成這件事。
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)勢(shì)
可以直接使用 chrome devtool
調(diào)試
直接支持 console/setTimeout/setInterval api
直接支持消息通信的
api
到此這篇關(guān)于WebWorker 封裝 JavaScript 沙箱詳情的文章就介紹到這了,更多相關(guān)WebWorker 封裝 JavaScript 沙箱內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
微信小程序 支付功能開(kāi)發(fā)錯(cuò)誤總結(jié)
這篇文章主要介紹了微信小程序 支付功能開(kāi)發(fā)錯(cuò)誤總結(jié)的相關(guān)資料,需要的朋友可以參考下2017-02-02Javascript使用integrity屬性進(jìn)行安全驗(yàn)證
這篇文章主要介紹了Javascript使用integrity屬性進(jìn)行安全驗(yàn)證,在html中,script標(biāo)簽可以通過(guò)src屬性引入一個(gè)js文件,引入的js文件可以是本地的,也可以是遠(yuǎn)程的,下面我們一起來(lái)看看文章詳細(xì)內(nèi)容2021-11-11微信小程序 聊天室簡(jiǎn)單實(shí)現(xiàn)
這篇文章主要介紹了微信小程序 聊天室簡(jiǎn)單實(shí)現(xiàn)的相關(guān)資料,需要的朋友可以參考下2017-04-04微信小程序 數(shù)據(jù)綁定及運(yùn)算的簡(jiǎn)單實(shí)例
這篇文章主要介紹了微信小程序 數(shù)據(jù)綁定的簡(jiǎn)單實(shí)例的相關(guān)資料,希望通過(guò)本文能幫助到大家,需要的朋友可以參考下2017-09-09JS前端二維數(shù)組生成樹(shù)形結(jié)構(gòu)示例詳解
這篇文章主要為大家介紹了JS前端二維數(shù)組生成樹(shù)形結(jié)構(gòu)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09JavaScript手寫(xiě)異步加法asyncAdd方法詳解
這篇文章主要為大家介紹了JavaScript手寫(xiě)異步加法asyncAdd方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08