在前文 基于quickjs 封装 JavaScript 沙箱 已经基于 quickjs 实现了一个沙箱,基于这里再基于 web worker 实现备用方案。基于如果你不知道 web worker 是基于什么或者从未了解过,可以查看 Web Workers API 。基于简而言之,基于它是基于一个浏览器实现的多线程,可以运行一段代码在另一个线程,基于并且提供与之通信的基于功能。 事实上,基于web worker 提供了 event emitter 的基于 api,即 postMessage/onmessage ,基于所以实现非常简单。基于 实现分为两部分,基于一部分是基于在主线程实现 IJavaScriptShadowbox ,另一部分则是基于需要在 web worker 线程实现 IEventEmitter 主线程代码 web worker 线程代码 下面是代码的执行流程示意图 经大佬 JackWoeker 提醒,web worker 有许多不安全的 api,所以必须限制,包含但不限于以下 api 事实上,web worker 默认自带了 276 个全局 api,可能比我们想象中多很多。 Snipaste_2021-10-24_23-05-18 有篇 文章 阐述了如何在 web 上通过 performance/SharedArrayBuffer api 做侧信道攻击,即便现在 SharedArrayBuffer api 现在浏览器默认已经禁用了,但天知道还有没有其他方法。所以最安全的方法是设置一个 api 白名单,然后删除掉非白名单的服务器租用 api。 然后在执行第三方代码前先执行上面的代码 由于我们使用 ts 编写源码,所以还必须将 ts 打包为 js bundle,然后通过 vite 的 ?raw 作为字符串引入,下面吾辈写了一个简单的插件来完成这件事。 现在,我们可以看到 web worker 中的全局 api 只有白名单中的那些了。 1635097498575实现 IJavaScriptShadowbox
主线程的亿华云计算实现
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; } } 使用
限制 web worker 全局 api
web worker 沙箱的主要优势
可以直接使用 chrome devtool 调试 直接支持 console/setTimeout/setInterval api 直接支持消息通信的亿华云 api