WebWorker 封装 JavaScript 沙箱详情

编辑: admin 分类: javascript 发布时间: 2021-11-17 来源:互联网
目录
  • 1、场景
  • 2、实现 IJavaScriptShadowbox
    • 2.1 主线程的实现
    • 2.2 web worker 线程的实现
  • 3、使用 WebWorkerShadowbox/WebWorkerEventEmitter
    • 4、限制 web worker 全局 api
      • 5、web worker 沙箱的主要优势

        1、场景

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

        2、实现 IJavaScriptShadowbox

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

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

        2.1 主线程的实现

        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 线程的实现

        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韩国站群服务器http://www.558idc.com/krzq.html(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"));
        
        
        

        下面是代码的执行流程示意图;web worker 沙箱实现使用示例代码的执行流程:

        4、限制 web worker 全局 api

        经大佬 JackWoeker 提醒,web worker 有许多不安全的 api,所以必须限制,包含但不限于以下 api

        • fetch
        • indexedDB
        • performance

        事实上,web worker 默认自带了 276 个全局 api,可能比我们想象中多很多。

        有篇 文章 阐述了如何在 web 上通过 performance/SharedArrayBuffer api 做侧信道攻击,即便现在 SharedArrayBuffer api 现在浏览器默认已经禁用了,但天知道还有没有其他方法。所以最安全的方法是设置一个 api 白名单,然后删除掉非白名单的 api。

        // whitelistWorkerGlobalScope.ts
        /**
         * 设定 web worker 运行时白名单,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);
        
        

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

        import beforeCode from "./whitelistWorkerGlobalScope.js?raw";
        
        export class WebWorkerShadowbox implements IJavaScriptShadowbox {
          destroy(): void {
            this.worker.terminate();
          }
        
          private worker!: Worker;
          eval(code: string): void {
            // 这行是关键
            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("构建完成: ", 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")]),
          ],
        });
        
        

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

        5、web worker 沙箱的主要优势

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

        到此这篇关于WebWorker 封装 JavaScript 沙箱详情的文章就介绍到这了,更多相关WebWorker 封装 JavaScript 沙箱内容请搜索hwidc以前的文章或继续浏览下面的相关文章希望大家以后多多支持hwidc!