WebWorker 封装 JavaScript 沙箱详情


Posted in Javascript onNovember 02, 2021

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(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 沙箱实现使用示例代码的执行流程:

WebWorker 封装 JavaScript 沙箱详情

4、限制 web worker 全局 api

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

  • fetch
  • indexedDB
  • performance

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

WebWorker 封装 JavaScript 沙箱详情

有篇 文章 阐述了如何在 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 只有白名单中的那些了。

WebWorker 封装 JavaScript 沙箱详情

5、web worker 沙箱的主要优势

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

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

Javascript 相关文章推荐
几个比较经典常用的jQuery小技巧
Mar 01 Javascript
js中的值类型和引用类型小结 文字说明与实例
Dec 12 Javascript
jquery插件开发注意事项小结
Jun 04 Javascript
给ListBox添加双击事件示例代码
Dec 02 Javascript
JavaScript通过function定义对象并给对象添加toString()方法实例分析
Mar 23 Javascript
JavaScript下的时间格式处理函数Date.prototype.format
Jan 27 Javascript
前端框架Vue.js构建大型应用浅析
Sep 12 Javascript
详解照片瀑布流效果(js,jquery分别实现与知识点总结)
Jan 01 Javascript
原生JS实现圣旨卷轴展开效果
Mar 06 Javascript
angular2路由之routerLinkActive指令【推荐】
May 30 Javascript
原生JavaScript实现滑动拖动验证的示例代码
Dec 06 Javascript
微信小程序实现翻牌抽奖动画
Sep 21 Javascript
quickjs 封装 JavaScript 沙箱详情
Nov 02 #Javascript
js 数组 fill() 填充方法
浅谈 JavaScript 沙箱Sandbox
详解 TypeScript 枚举类型
Nov 02 #Javascript
前端JavaScript大管家 package.json
JavaScript 原型与原型链详情
javascript实现计算器功能详解流程
You might like
Apache, PHP在Windows 9x/NT下的安装与配置 (一)
2006/10/09 PHP
PHP5.3与5.5废弃与过期函数整理汇总
2014/07/10 PHP
在html文件中也可以执行php语句的方法
2015/04/09 PHP
THINKPHP在添加数据的时候获取主键id的值方法
2017/04/03 PHP
[原创]PHP global全局变量经典应用与注意事项分析【附$GLOBALS用法对比】
2019/07/12 PHP
js类后台管理菜单类-MenuSwitch
2007/09/12 Javascript
javascript 当前日期加(天、周、月、年)
2009/08/09 Javascript
js对象的构造和继承实现代码
2010/12/05 Javascript
javascript显示用户停留时间的简单实例
2013/08/05 Javascript
jquery select 设置默认选中的示例代码
2014/02/07 Javascript
原生javascript实现匀速运动动画效果
2016/02/26 Javascript
AngularJS ui-router (嵌套路由)实例
2017/03/10 Javascript
nodejs中模块定义实例详解
2017/03/18 NodeJs
nodejs前端自动化构建环境的搭建
2017/07/26 NodeJs
详解Vue iview IE浏览器不兼容报错(Iview Bable polyfill)
2019/01/07 Javascript
Python FTP操作类代码分享
2014/05/13 Python
零基础写python爬虫之使用urllib2组件抓取网页内容
2014/11/04 Python
Python采用socket模拟TCP通讯的实现方法
2014/11/19 Python
Python3.5面向对象编程图文与实例详解
2019/04/24 Python
python使用Paramiko模块实现远程文件拷贝
2019/04/30 Python
Python实现直播推流效果
2019/11/26 Python
升级keras解决load_weights()中的未定义skip_mismatch关键字问题
2020/06/12 Python
欧舒丹比利时官网:L’OCCITANE比利时
2017/04/25 全球购物
全球酒店比价网:HotelsCombined
2017/06/20 全球购物
行政管理专业推荐信
2013/11/02 职场文书
开业庆典邀请函
2014/01/08 职场文书
幼儿园毕业家长感言
2014/02/10 职场文书
幼教求职信
2014/03/12 职场文书
农村党支部书记四风问题个人对照检查材料
2014/09/21 职场文书
领导班子奢靡之风查摆问题及整改措施
2014/09/27 职场文书
安全生产工作汇报
2014/10/28 职场文书
教师先进个人材料
2014/12/17 职场文书
幼儿园教师节感谢信
2015/01/23 职场文书
设备技术员岗位职责
2015/04/11 职场文书
运动会宣传语
2015/07/13 职场文书
微信小程序 根据不同用户切换不同TabBar
2022/04/21 Javascript