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 相关文章推荐
用javascript实现的支持lrc歌词的播放器
May 17 Javascript
图片自动缩小 点击放大
Jul 07 Javascript
AJAX的跨域与JSONP(为文章自动添加短址的功能)
Jan 17 Javascript
用Javascript获取页面元素的具体位置
Dec 09 Javascript
jQuery插件slick实现响应式移动端幻灯片图片切换特效
Apr 12 Javascript
javascript弹性运动效果简单实现方法
Jan 08 Javascript
jquery判断checkbox是否选中及改变checkbox状态的实现方法
May 26 Javascript
Bootstrap select下拉联动(jQuery cxselect)
Jan 04 Javascript
bootstrap-table组合表头的实现方法
Sep 07 Javascript
利用js实现前后台传送Json的示例代码
Mar 29 Javascript
谈谈JavaScript中super(props)的重要性
Feb 12 Javascript
JS实现滚动条触底加载更多
Sep 19 Javascript
quickjs 封装 JavaScript 沙箱详情
Nov 02 #Javascript
js 数组 fill() 填充方法
浅谈 JavaScript 沙箱Sandbox
详解 TypeScript 枚举类型
Nov 02 #Javascript
前端JavaScript大管家 package.json
JavaScript 原型与原型链详情
javascript实现计算器功能详解流程
You might like
深入解析PHP 5.3.x 的strtotime() 时区设定 警告信息修复
2013/08/05 PHP
mac下安装nginx和php
2013/11/04 PHP
PHP入门教程之面向对象的特性分析(继承,多态,接口,抽象类,抽象方法等)
2016/09/11 PHP
PHP连续签到功能实现方法详解
2019/12/04 PHP
重定向实现代码
2006/11/20 Javascript
B/S开发中常用javaScript技术与代码
2007/03/09 Javascript
用jQuery扩展自写的 UI导航
2010/01/13 Javascript
jQuery timers计时器简单应用说明
2010/10/28 Javascript
javascript中的对象创建 实例附注释
2011/02/08 Javascript
js中更短的 Array 类型转换
2011/10/30 Javascript
(跨浏览器基础事件/浏览器检测/判断浏览器)经验代码分享
2013/01/24 Javascript
js控制表单奇偶行样式的简单方法
2013/07/31 Javascript
制作jquery遮罩层效果导航菜单代码分享
2013/12/25 Javascript
JavaScript中的call方法和apply方法使用对比
2015/08/12 Javascript
React操作真实DOM实现动态吸底部的示例
2017/10/23 Javascript
微信小程序实现动态改变view标签宽度和高度的方法【附demo源码下载】
2017/12/05 Javascript
基于 Immutable.js 实现撤销重做功能的实例代码
2018/03/01 Javascript
vue.js中npm安装教程图解
2018/04/10 Javascript
微信小程序实现单列下拉菜单效果
2019/04/25 Javascript
JavaScript对象原型链原理解析
2020/01/22 Javascript
使用jquery实现轮播图效果
2021/01/02 jQuery
使用python的pexpect模块,实现远程免密登录的示例
2019/02/14 Python
Python基本类型的连接组合和互相转换方式(13种)
2019/12/16 Python
解决Django提交表单报错:CSRF token missing or incorrect的问题
2020/03/13 Python
Django实现前台上传并显示图片功能
2020/05/29 Python
英国时尚饰品和发饰购物网站:Claire’s
2017/07/04 全球购物
会计专业毕业生自我评价
2013/09/25 职场文书
会计电算化专业毕业生推荐信
2013/12/24 职场文书
电台编导求职信
2014/05/06 职场文书
自动化专业毕业生求职信
2014/06/18 职场文书
敬老月活动总结
2014/08/28 职场文书
爱心募捐通知范文
2015/04/27 职场文书
演讲开场白和结束语
2015/05/29 职场文书
四年级作文之说明文作文
2019/10/14 职场文书
MongoDB数据库常用的10条操作命令
2021/06/18 MongoDB
CentOS7安装GlusterFS集群以及相关配置
2022/04/12 Servers