Web Worker 沙箱:安全执行用户自定义代码

源码

Web Worker 沙箱:安全执行用户自定义代码

在可视化平台中,用户可以:
• 配置一个数据接口
• 写一段数据转换代码
• 甚至直接写一段用于渲染的 JS 逻辑

这带来两个问题:
1. 安全:恶意代码可能访问主线程资源、篡改 DOM、窃取数据
2. 稳定性:死循环、长耗时计算可能卡死主线程

  1. 为什么选 Web Worker 作为代码沙箱

优点:
• 独立线程,隔离主线程 DOM
• 只能通过消息通信(postMessage)交换数据
• 可随时 terminate() 中止执行(解决死循环卡死问题)
• 支持 ES 模块语法(现代浏览器)

限制:
• 不能直接访问 DOM、window、document
• 与主线程共享内存有限(只能通过 SharedArrayBuffer 或传值)

  1. 沙箱架构

主线程(Renderer)
├── 传入接口返回的数据
├── 传入自定义 JS 代码(字符串)
├── 传入上下文参数(hooks、utils)
↓ postMessage
Worker(Sandbox)
├── 安全运行用户代码
├── 调用提供的 API(白名单)
└── 返回转换后的数据

  1. Web Worker 沙箱实现

3.1 Worker 脚本(sandbox.worker.ts)

// sandbox.worker.ts
import * as Comlink from 'comlink';

interface SandboxAPI {
  runUserCode(code: string, inputData: any, context?: Record<string, any>): Promise<any>;
}

const api: SandboxAPI = {
  async runUserCode(code, inputData, context = {}) {
    return new Promise((resolve, reject) => {
      try {
        const fn = new Function('data', 'context', `
          "use strict";
          ${code.includes('return') ? `return (${code});` : code}
        `);
        const result = fn(inputData, context);
        resolve(result);
      } catch (err) {
        reject(err);
      }
    });
  }
};

Comlink.expose(api);

3.2 主线程使用 Worker

// sandboxClient.ts
import * as Comlink from 'comlink';
import SandboxWorker from './sandbox.worker?worker';

let sandboxInstance: Comlink.Remote<any> | null = null;

export async function getSandbox() {
  if (!sandboxInstance) {
    const worker = new SandboxWorker();
    sandboxInstance = Comlink.wrap(worker);
  }
  return sandboxInstance;
}

// 调用
(async () => {
  const sandbox = await getSandbox();
  const result = await sandbox.runUserCode(
    `return data.map(item => ({ ...item, name: item.name.toUpperCase() }))`,
    [{ name: 'apple' }, { name: 'banana' }],
    { utils: { toUpper: (str: string) => str.toUpperCase() } }
  );
  console.log('转换结果', result);
})();

  1. 防止死循环卡死

虽然 Worker 在单独线程运行,不会卡主线程,但如果代码进入死循环,会一直占用 CPU。
解决方法:
1. 超时控制:主线程在一定时间后调用 worker.terminate() 强制结束
2. 重启 Worker:执行结束后可重置,避免内存泄露

示例:

export async function runWithTimeout(code: string, data: any, timeout = 2000) {
  const worker = new SandboxWorker();
  const sandbox = Comlink.wrap<any>(worker);
  const timer = setTimeout(() => {
    worker.terminate();
    throw new Error('执行超时');
  }, timeout);
  const result = await sandbox.runUserCode(code, data);
  clearTimeout(timer);
  worker.terminate();
  return result;
}

  1. 支持主线程传入 API(Hooks/Utils)

我们可以在调用时传递 白名单 API:

const hooks = {
  formatDate: (d: string) => new Date(d).toLocaleDateString(),
  sum: (arr: number[]) => arr.reduce((a, b) => a + b, 0)
};

await sandbox.runUserCode(`
  return {
    total: context.sum(data.numbers),
    date: context.formatDate(data.date)
  }
`, { numbers: [1, 2, 3], date: '2025-08-09' }, hooks);

这样用户代码只能调用 context 中暴露的 API,无法访问主线程的任意资源。

  1. 对比 iframe 沙箱与 Worker 沙箱

特性 iframe 沙箱 Web Worker 沙箱
用途 渲染 UMD 组件(UI 隔离) 执行用户 JS(逻辑隔离)
能访问 DOM ✅(iframe 内) ❌
线程隔离 ❌(同线程) ✅(多线程)
终止执行 ❌ ✅(terminate)
版本隔离 ✅ ❌(不处理 UI)

结论:
• iframe 沙箱:解决 UI 版本冲突、样式污染
• Web Worker 沙箱:解决逻辑执行安全、死循环控制
• 两者结合:UI 与逻辑双重隔离

  1. 总结

我们用 Web Worker + Comlink 实现了一个可复用的 用户自定义代码执行沙箱:
• 独立线程运行,保护主线程性能
• 支持传入数据和安全 API
• 超时控制避免死循环
• 可结合 iframe 沙箱实现 UI + 逻辑的全隔离

到这里,我们的 可视化编辑平台安全体系 已经完整成型:
1. iframe 沙箱 → 渲染 UMD 组件(隔离 UI 与版本冲突)
2. Worker 沙箱 → 执行自定义代码(隔离逻辑与死循环)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

今天也想MK代码

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值