基于 WebWorker 封装 JavaScript 沙箱

开发 前端
如果你不知道 web worker 是什么或者从未了解过,可以查看 Web Workers API 。简而言之,它是一个浏览器实现的多线程,可以运行一段代码在另一个线程,并且提供与之通信的功能。

  [[430837]]

在前文 基于quickjs 封装 JavaScript 沙箱 已经基于 quickjs 实现了一个沙箱,这里再基于 web worker 实现备用方案。如果你不知道 web worker 是什么或者从未了解过,可以查看 Web Workers API 。简而言之,它是一个浏览器实现的多线程,可以运行一段代码在另一个线程,并且提供与之通信的功能。

实现 IJavaScriptShadowbox

事实上,web worker 提供了 event emitter 的 api,即 postMessage/onmessage ,所以实现非常简单。

实现分为两部分,一部分是在主线程实现 IJavaScriptShadowbox ,另一部分则是需要在 web worker 线程实现 IEventEmitter

主线程的实现

  1. import { IJavaScriptShadowbox } from "./IJavaScriptShadowbox"
  2.  
  3. export class WebWorkerShadowbox implements IJavaScriptShadowbox { 
  4.   destroy(): void { 
  5.     this.worker.terminate(); 
  6.   } 
  7.  
  8.   private worker!: Worker; 
  9.   eval(code: string): void { 
  10.     const blob = new Blob([code], { type: "application/javascript" }); 
  11.     this.worker = new Worker(URL.createObjectURL(blob), { 
  12.       credentials: "include"
  13.     }); 
  14.     this.worker.addEventListener("message", (ev) => { 
  15.       const msg = ev.data as { channel: string; data: any }; 
  16.       // console.log('msg.data: ', msg) 
  17.       if (!this.listenerMap.has(msg.channel)) { 
  18.         return
  19.       } 
  20.       this.listenerMap.get(msg.channel)!.forEach((handle) => { 
  21.         handle(msg.data); 
  22.       }); 
  23.     }); 
  24.   } 
  25.  
  26.   private readonly listenerMap = new Map<string, ((data: any) => void)[]>(); 
  27.   emit(channel: string, data: any): void { 
  28.     this.worker.postMessage({ 
  29.       channel: channel, 
  30.       data, 
  31.     }); 
  32.   } 
  33.   on(channel: string, handle: (data: any) => void): void { 
  34.     if (!this.listenerMap.has(channel)) { 
  35.       this.listenerMap.set(channel, []); 
  36.     } 
  37.     this.listenerMap.get(channel)!.push(handle); 
  38.   } 
  39.   offByChannel(channel: string): void { 
  40.     this.listenerMap.delete(channel); 
  41.   } 

web worker 线程的实现

  1. import { IEventEmitter } from "./IEventEmitter"
  2.  
  3. export class WebWorkerEventEmitter implements IEventEmitter { 
  4.   private readonly listenerMap = new Map<string, ((data: any) => void)[]>(); 
  5.  
  6.   emit(channel: string, data: any): void { 
  7.     postMessage({ 
  8.       channel: channel, 
  9.       data, 
  10.     }); 
  11.   } 
  12.  
  13.   on(channel: string, handle: (data: any) => void): void { 
  14.     if (!this.listenerMap.has(channel)) { 
  15.       this.listenerMap.set(channel, []); 
  16.     } 
  17.     this.listenerMap.get(channel)!.push(handle); 
  18.   } 
  19.  
  20.   offByChannel(channel: string): void { 
  21.     this.listenerMap.delete(channel); 
  22.   } 
  23.  
  24.   init() { 
  25.     onmessage = (ev) => { 
  26.       const msg = ev.data as { channel: string; data: any }; 
  27.       if (!this.listenerMap.has(msg.channel)) { 
  28.         return
  29.       } 
  30.       this.listenerMap.get(msg.channel)!.forEach((handle) => { 
  31.         handle(msg.data); 
  32.       }); 
  33.     }; 
  34.   } 
  35.  
  36.   destroy() { 
  37.     this.listenerMap.clear(); 
  38.     onmessage = null
  39.   } 

使用

主线程代码

  1. const shadowbox: IJavaScriptShadowbox = new WebWorkerShadowbox(); 
  2. shadowbox.on("hello", (name: string) => { 
  3.   console.log(`hello ${name}`); 
  4. }); 
  5. // 这里的 code 指的是下面 web worker 线程的代码 
  6. shadowbox.eval(code); 
  7. shadowbox.emit("open"); 

web worker 线程代码

  1. const em = new WebWorkerEventEmitter(); 
  2. 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。

  1. // whitelistWorkerGlobalScope.ts 
  2.  
  3. /** 
  4.  
  5. * 设定 web worker 运行时白名单,ban 掉所有不安全的 api 
  6.  
  7. */ 
  8.  
  9. export function whitelistWorkerGlobalScope(list: PropertyKey[]) { 
  10.  
  11. const whitelist = new Set(list); 
  12.  
  13. const all = Reflect.ownKeys(globalThis); 
  14.  
  15. all.forEach((k) => { 
  16.  
  17. if (whitelist.has(k)) { 
  18.  
  19. return
  20.  
  21.  
  22. if (k === "window") { 
  23.  
  24. console.log("window: ", k); 
  25.  
  26.  
  27. Reflect.deleteProperty(globalThis, k); 
  28.  
  29. }); 
  30.  
  31.  
  32. /** 
  33.  
  34. * 全局值的白名单 
  35.  
  36. */ 
  37.  
  38. const whitelist: ( 
  39.  
  40. | keyof typeof global 
  41.  
  42. | keyof WindowOrWorkerGlobalScope 
  43.  
  44. "console" 
  45.  
  46. )[] = [ 
  47.  
  48. "globalThis"
  49.  
  50. "console"
  51.  
  52. "setTimeout"
  53.  
  54. "clearTimeout"
  55.  
  56. "setInterval"
  57.  
  58. "clearInterval"
  59.  
  60. "postMessage"
  61.  
  62. "onmessage"
  63.  
  64. "Reflect"
  65.  
  66. "Array"
  67.  
  68. "Map"
  69.  
  70. "Set"
  71.  
  72. "Function"
  73.  
  74. "Object"
  75.  
  76. "Boolean"
  77.  
  78. "String"
  79.  
  80. "Number"
  81.  
  82. "Math"
  83.  
  84. "Date"
  85.  
  86. "JSON"
  87.  
  88. ]; 
  89.  
  90. whitelistWorkerGlobalScope(whitelist); 

然后在执行第三方代码前先执行上面的代码

  1. import beforeCode from "./whitelistWorkerGlobalScope.js?raw"
  2.  
  3. export class WebWorkerShadowbox implements IJavaScriptShadowbox { 
  4.  
  5. destroy(): void { 
  6.  
  7. this.worker.terminate(); 
  8.  
  9.  
  10. private worker!: Worker; 
  11.  
  12. eval(code: string): void { 
  13.  
  14. // 这行是关键 
  15.  
  16. const blob = new Blob([beforeCode + "\n" + code], { 
  17.  
  18. type: "application/javascript"
  19.  
  20. }); 
  21.  
  22. // 其他代码。。。 
  23.  
  24.  

由于我们使用 ts 编写源码,所以还必须将 ts 打包为 js bundle,然后通过 vite 的 ?raw 作为字符串引入,下面吾辈写了一个简单的插件来完成这件事。

  1. import { defineConfig, Plugin } from "vite"
  2.  
  3. import reactRefresh from "@vitejs/plugin-react-refresh"
  4.  
  5. import checker from "vite-plugin-checker"
  6.  
  7. import { build } from "esbuild"
  8.  
  9. import * as path from "path"
  10.  
  11. export function buildScript(scriptList: string[]): Plugin { 
  12.  
  13. const _scriptList = scriptList.map((src) => path.resolve(src)); 
  14.  
  15. async function buildScript(src: string) { 
  16.  
  17. await build({ 
  18.  
  19. entryPoints: [src], 
  20.  
  21. outfile: src.slice(0, src.length - 2) + "js"
  22.  
  23. format: "iife"
  24.  
  25. bundle: true
  26.  
  27. platform: "browser"
  28.  
  29. sourcemap: "inline"
  30.  
  31. allowOverwrite: true
  32.  
  33. }); 
  34.  
  35. console.log("构建完成: ", path.relative(path.resolve(), src)); 
  36.  
  37.  
  38. return { 
  39.  
  40. name: "vite-plugin-build-script"
  41.  
  42. async configureServer(server) { 
  43.  
  44. server.watcher.add(_scriptList); 
  45.  
  46. const scriptSet = new Set(_scriptList); 
  47.  
  48. server.watcher.on("change", (filePath) => { 
  49.  
  50. // console.log('change: ', filePath) 
  51.  
  52. if (scriptSet.has(filePath)) { 
  53.  
  54. buildScript(filePath); 
  55.  
  56.  
  57. }); 
  58.  
  59. }, 
  60.  
  61. async buildStart() { 
  62.  
  63. // console.log('buildStart: ', this.meta.watchMode) 
  64.  
  65. if (this.meta.watchMode) { 
  66.  
  67. _scriptList.forEach((src) => this.addWatchFile(src)); 
  68.  
  69.  
  70. await Promise.all(_scriptList.map(buildScript)); 
  71.  
  72. }, 
  73.  
  74. }; 
  75.  
  76.  
  77. // https://vitejs.dev/config/ 
  78.  
  79. export default defineConfig({ 
  80.  
  81. plugins: [ 
  82.  
  83. reactRefresh(), 
  84.  
  85. checker({ typescript: true }), 
  86.  
  87. buildScript([path.resolve("src/utils/app/whitelistWorkerGlobalScope.ts")]), 
  88.  
  89. ], 
  90.  
  91. }); 

现在,我们可以看到 web worker 中的全局 api 只有白名单中的那些了。

1635097498575

web worker 沙箱的主要优势

  • 可以直接使用 chrome devtool 调试
  • 直接支持 console/setTimeout/setInterval api
  • 直接支持消息通信的 api

 

责任编辑:张燕妮 来源: rxliuli blog
相关推荐

2021-12-29 11:38:59

JS前端沙箱

2015-11-12 10:03:34

前端H5web

2021-04-09 08:51:32

Web WorkerJavaScript微前端

2016-09-06 21:37:41

2011-05-25 10:21:44

Javascript

2017-07-17 06:46:06

2019-02-13 14:58:43

cssjavascript前端

2022-04-13 09:28:19

JavaScripiframe开发

2024-03-12 08:44:56

WebWorkerTypeScript语法

2010-09-15 09:03:44

JavaScript

2019-03-14 15:40:13

JavaScript CSS 工具

2018-01-30 18:49:16

前端JavascriptCSS

2024-07-18 08:59:39

CanvasWebWorker代码

2021-12-29 22:29:10

JavaScript前端数组

2015-05-06 10:02:26

2019-03-22 08:25:47

沙箱网络安全恶意软件

2010-08-11 13:46:01

Flex安全沙箱

2018-12-14 11:30:00

JavaScript编程前端

2009-07-14 11:23:06

CSS技术概览

2010-07-29 15:36:23

Flex安全沙箱
点赞
收藏

51CTO技术栈公众号