上一篇文章基于quickjs封装JavaScript沙箱,已经实现了基于quickjs的沙箱,这里介绍基于webworker的替代方案。如果您不知道或从来不知道什么是WebWorker,请查看WebWorkersAPI。简而言之,它是一种多线程的浏览器实现,可以在另一个线程中运行一段代码,并提供与之通信的能力。实现IJavaScriptShadowbox其实webworkers提供了事件发射器API,即postMessage/onmessage,所以实现起来非常简单。实现分为两部分,一是在主线程中实现IJavaScriptShadowbox,二是在webworker线程中实现IEventEmitter主线程。从“./IJavaScriptShadowbox”导入{IJavaScriptShadowbox};exportclassWebWorkerShadowboximplementsIJavaScriptShadowbox{destroy():void{this.worker.terminate();}privateworker!:Worker;eval(code:string):void{constblob=newBlob([code],{type:"application/javascript"});this.worker=newWorker(URL.createObjectURL(blob),{credentials:"include",});this.worker.addEventListener("message",(ev)=>{constmsg=ev.dataas{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);});});}privatereadonlylistenerMap=newMapvoid)[]>();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);}}webworker线程的实际导入{IEventEmitter}from"./IEventEmitter";exportclassWebWorkerEventEmitterimplementsIEventEmitter{privatereadonlylistenerMap=newMapvoid)[]>();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.设置(通道,[]);}this.listenerMap.get(通道)!.push(句柄);}offByChannel(通道:字符串):void{this.listenerMap.delete(通道);}init(){onmessage=(ev)=>{constmsg=ev.dataas{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;}}使用主线程序代码constshadowbox:IJavaScriptShadowbox=newWebWorkerShadowbox();shadowbox.on("hello",(name:string)=>{console.log(`hello${name}`);});//这里的代码参考下面webworker线程的代码shadowbox.eval(code);shadowbox.emit("open");webworker线程代码constem=newWebWorkerEventEmitter();em.on("open",()=>em.emit("hello","liuli"));下面是代码执行流程示意图限制webworker的全局API老板JackWoeker提醒,webworker有很多不安全的API,所以一定要限制,包括但不限于下面的APIfetchindexedDBperformance事实上,webworker默认自带276Snipaste_2021-10-24_23-05-18有一篇文章解释了如何通过performance/SharedArrayBufferapi对web进行边信道攻击,尽管SharedArrayBufferapi现在是在浏览器中默认禁用,但天知道是否有其他方法。所以最稳妥的办法就是设置一个api白名单,然后删除不在白名单内的api。//whitelistWorkerGlobalScope.ts/***设置webworker运行时白名单,禁止所有不安全的api*/exportfunctionwhitelistWorkerGlobalScope(list:PropertyKey[]){constwhitelist=newSet(list);constall=Reflect.ownKeys(globalThis);all.forEach((k)=>{if(whitelist.has(k)){return;}if(k==="window"){console.log("window:",k);}Reflect.deleteProperty(globalThis,k);});}/***全局值白名单*/constwhitelist:(|keyoftypeofglobal|keyofWindowOrWorkerGlobalScope|"console")[]=["globalThis","console","setTimeout","clearTimeout","setInterval","clearInterval","postMessage","onmessage","Reflect","Array","Map","Set","Function","Object","Boolean","String","Number"","Math","Date","JSON",];whitelistWorkerGlobalScope(whitelist);然后在执行第三方代码importbeforeCodefrom之前先执行上面的代码./whitelistWorkerGlobalScope.js?raw";exportclassWebWorkerShadowboximplementsIJavaScriptShadowbox{destroy():void{this.worker.terminate();}privateworker!:Worker;eval(code:string):void{//这一行是关键constblob=newBlob([beforeCode+"\n"+code],{type:"application/javascript",});//其他代码...}}由于我们是用ts来写源码的,所以还要把ts打包成jsbundle,然后通过vite的?raw作为字符串引入,下面我们写了一个简单的插件来做这个。import{defineConfig,Plugin}from"vite";importreactRefreshfrom"@vitejs/plugin-react-refresh";importcheckerfrom"vite-plugin-checker";导入{build}from"esbuild";import*aspathfrom"path";exportfunctionbuildScript(scriptList:string[]):Plugin{const_scriptList=scriptList.map((src)=>path.resolve(src));asyncfunctionbuildScript(src:string){awaitbuild({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",asyncconfigureServer(server){server.watcher.add(_scriptList);constscriptSet=newSet(_scriptList);server.watcher.on("change",(filePath)=>{//console.log('change:',filePath)if(scriptSet.has(filePath)){buildScript(filePath);}});},asyncbuildStart(){//控制台。log('buildStart:',this.meta.watchMode)if(this.meta.watchMode){_scriptList.forEach((src)=>this.addWatchFile(src));}awaitPromise.all(_scriptList.map(buildScript));},};}//https://vitejs.dev/config/exportdefaultdefineConfig({plugins:[reactRefresh(),checker({typescript:true}),buildScript([path.resolve("src/utils/app/whitelistWorkerGlobalScope.ts")]),],});现在,我们可以看到webworker中的全局api只有白名单中的1635097498575webworker沙箱的主要优点是可以直接使用chromedevtool调试,直接支持console/setTimeout/setIntervalapiAPIs直接支持消息通信