构建安全的 JavaScript 沙箱执行环境:从 iframe 到 ShadowRealm 完整方案

深入解析 JavaScript 沙箱隔离技术,涵盖 iframe、Web Worker、vm 模块、ShadowRealm 等方案的原理、代码实现与安全对比,帮助开发者在浏览器和 Node.js 中安全执行不受信任的代码。

安全与密码 2026-06-07 12 分钟

2025 年,一个流行的 npm 包 everything-sdk 被发现暗中窃取环境变量和钱包私钥,影响超过 12 万个开发项目。这类供应链攻击的核心问题只有一个:我们如何安全地执行不受信任的 JavaScript 代码? 无论是在线代码编辑器、插件系统、还是 AI 生成代码的运行环境,JavaScript 沙箱(Sandbox)都是每个前端工程师必须理解的安全基础设施。

📌 记住: JavaScript 的安全性不是"可选项"——当你允许用户输入任意代码时,沙箱就是你的最后一道防线。

🔐 一、沙箱隔离的核心原理与威胁模型

1.1 为什么需要沙箱?

在讨论具体方案之前,先明确沙箱要防御什么。一个不受信任的代码片段可能执行以下攻击:

  • 全局对象篡改:修改 Object.prototype、重写 fetch、劫持 XMLHttpRequest
  • 无限循环 / 资源耗尽while(true){} 耗尽 CPU,new Array(10^9) 耗尽内存
  • 信息窃取:读取 document.cookie、访问 localStorage、探测内网服务
  • XSS 注入:通过 innerHTMLeval 等方式注入恶意脚本

沙箱的核心目标是实现三层隔离

隔离层 目标 技术手段
代码隔离 不同代码的执行上下文互不干扰 独立的全局对象、作用域链
资源隔离 限制 CPU、内存、网络访问 超时机制、内存上限、网络白名单
权限隔离 禁止访问敏感 API Proxy 拦截、API 白名单

1.2 同源策略 vs 沙箱隔离

很多开发者混淆了浏览器的同源策略(Same-Origin Policy)和沙箱隔离。它们解决的是不同的问题:

  • ✅ 同源策略:防止不同域名的页面互相读取数据
  • ❌ 同源策略不能阻止:同一页面内的恶意代码篡改全局对象
// ❌ 同源策略无法防御这种攻击
// 恶意代码在同一个页面内执行
Object.defineProperty(Object.prototype, 'isAdmin', {
  get() { return true; }
});
// 所有对象都会返回 isAdmin === true

所以即使在同一个域名下,执行不受信任的代码仍然需要沙箱。

🚀 二、浏览器端沙箱方案实战

2.1 iframe Sandbox:最成熟的隔离方案

iframe 的 sandbox 属性是浏览器原生提供的最强隔离机制。它创建一个完全独立的浏览上下文,有独立的 windowdocument、全局对象。

<!-- 基础 iframe 沙箱配置 -->
<iframe
  sandbox="allow-scripts"
  srcdoc="<script>console.log('isolated!')</script>"
></iframe>

sandbox 属性的可选值:

属性值 作用 安全性
(无) 最严格:禁止一切 ✅ 最安全
allow-scripts 允许执行脚本 ✅ 安全
allow-same-origin 允许访问父页面存储 ❌ 危险
allow-forms 允许提交表单 ⚠️ 中等
allow-popups 允许弹出窗口 ⚠️ 中等

⚠️ 警告: 绝对不要同时使用 allow-scriptsallow-same-origin!这等于移除了沙箱的所有保护。

下面是一个完整的 iframe 沙箱通信实现:

// 主页面:创建沙箱并执行代码
class IframeSandbox {
  constructor() {
    this.iframe = document.createElement('iframe');
    this.iframe.sandbox = 'allow-scripts';
    this.iframe.style.display = 'none';
    document.body.appendChild(this.iframe);
    this.messageId = 0;
    this.pending = new Map();
    
    window.addEventListener('message', (e) => {
      if (e.source !== this.iframe.contentWindow) return;
      const { id, result, error } = e.data;
      const resolve = this.pending.get(id);
      if (resolve) {
        this.pending.delete(id);
        error ? resolve(Promise.reject(new Error(error))) : resolve(result);
      }
    });
  }

  // 执行代码并返回结果
  execute(code, timeout = 5000) {
    return new Promise((resolve, reject) => {
      const id = ++this.messageId;
      this.pending.set(id, resolve);
      
      // 设置超时,防止无限循环
      const timer = setTimeout(() => {
        this.pending.delete(id);
        // 销毁并重建 iframe 来终止执行
        this.destroy();
        this.iframe = document.createElement('iframe');
        this.iframe.sandbox = 'allow-scripts';
        this.iframe.style.display = 'none';
        document.body.appendChild(this.iframe);
        reject(new Error('代码执行超时'));
      }, timeout);

      this.iframe.srcdoc = `
        <script>
          try {
            const fn = new Function(${JSON.stringify(code)});
            const result = fn();
            parent.postMessage({ id: ${id}, result }, '*');
          } catch(e) {
            parent.postMessage({ id: ${id}, error: e.message }, '*');
          }
        <\/script>
      `;

      this.pending.get(id)((val) => {
        clearTimeout(timer);
        resolve(val);
      });
    });
  }

  destroy() {
    this.iframe.remove();
    this.iframe = null;
  }
}

// 使用示例
const sandbox = new IframeSandbox();
const result = await sandbox.execute('return 1 + 2 + 3');
console.log(result); // 6

iframe 方案的优缺点:

  • ✅ 浏览器原生支持,隔离级别最高
  • ✅ 独立的 JavaScript 执行上下文
  • ✅ 可以限制网络、存储、弹窗等权限
  • ❌ 通信开销较大(postMessage 序列化)
  • ❌ 无法直接访问父页面的 DOM
  • ❌ 创建和销毁 iframe 有一定性能开销

2.2 Web Worker 沙箱:无 DOM 访问的隔离执行

Web Worker 提供了独立的线程环境,天然没有 documentwindow 等 DOM API,适合执行纯计算逻辑的不受信任代码。

// worker-sandbox.js - Worker 内部代码
self.onmessage = function(e) {
  const { id, code, timeout } = e.data;
  
  // 禁止访问危险 API
  const blocked = ['importScripts', 'fetch', 'XMLHttpRequest'];
  const original = {};
  blocked.forEach(name => {
    original[name] = self[name];
    self[name] = undefined;
  });

  try {
    // 使用 Function 构造器执行代码,限制作用域
    const fn = new Function('self', 'globalThis', `
      "use strict";
      ${code}
    `);
    const result = fn(undefined, undefined);
    
    // 恢复被禁用的 API
    blocked.forEach(name => {
      self[name] = original[name];
    });

    self.postMessage({ id, result });
  } catch (error) {
    blocked.forEach(name => {
      self[name] = original[name];
    });
    self.postMessage({ id, error: error.message });
  }
};
// 主页面:Worker 沙箱管理器
class WorkerSandbox {
  constructor() {
    this.worker = new Worker('worker-sandbox.js');
    this.messageId = 0;
    this.pending = new Map();

    this.worker.onmessage = (e) => {
      const { id, result, error } = e.data;
      const { resolve, reject, timer } = this.pending.get(id);
      clearTimeout(timer);
      this.pending.delete(id);
      error ? reject(new Error(error)) : resolve(result);
    };
  }

  execute(code, timeout = 3000) {
    return new Promise((resolve, reject) => {
      const id = ++this.messageId;
      const timer = setTimeout(() => {
        this.pending.delete(id);
        this.worker.terminate();
        this.worker = new Worker('worker-sandbox.js');
        reject(new Error('执行超时'));
      }, timeout);

      this.pending.set(id, { resolve, reject, timer });
      this.worker.postMessage({ id, code, timeout });
    });
  }

  terminate() {
    this.worker.terminate();
  }
}

💡 提示: Web Worker 的 importScripts() 是一个潜在的安全风险,因为它可以加载任意外部脚本。在沙箱中应该禁用它。

2.3 Proxy 拦截:细粒度的全局对象保护

如果你需要在主线程中隔离代码(不使用 iframe 或 Worker),可以通过 Proxy 拦截全局对象的访问。这是很多在线代码编辑器(如 CodeSandbox)采用的策略。

// 创建一个安全的沙箱全局对象
function createSandboxGlobal(allowedAPIs = {}) {
  // 需要屏蔽的危险 API
  const blocked = new Set([
    'eval', 'Function', 'importScripts',
    'XMLHttpRequest', 'fetch',
    'WebSocket', 'Worker',
    'navigator', 'location',
    'document', 'localStorage', 'sessionStorage',
    'indexedDB', 'cookie',
  ]);

  // 基础全局对象(数学、日期等安全 API)
  const safeGlobals = {
    console: { log: console.log, warn: console.warn, error: console.error },
    Math, Number, String, Boolean, Array, Object,
    JSON, Date, RegExp, Map, Set, WeakMap, WeakSet,
    Promise, Symbol, Proxy, Reflect,
    parseInt, parseFloat, isNaN, isFinite,
    encodeURIComponent, decodeURIComponent,
    btoa, atob, structuredClone,
    ...allowedAPIs,
  };

  const globalProxy = new Proxy(Object.create(null), {
    get(target, prop) {
      if (prop === Symbol.unscopables) return undefined;
      if (prop in target) return target[prop];
      if (prop in safeGlobals) return safeGlobals[prop];
      // 禁止访问被屏蔽的 API
      if (blocked.has(prop)) {
        throw new ReferenceError(`"${prop}" 在沙箱中不可用`);
      }
      return undefined;
    },
    set(target, prop, value) {
      target[prop] = value;
      return true;
    },
    has(target, prop) {
      return prop in target || prop in safeGlobals;
    },
  });

  return globalProxy;
}

// 在沙箱中执行代码
function runInSandbox(code, globals = {}) {
  const sandboxGlobal = createSandboxGlobal(globals);
  
  // 使用 with 语句将沙箱全局对象作为作用域
  // 注意:with 语句在严格模式下不可用,这里用 Function 构造器绕过
  const wrappedCode = `
    with (sandbox) {
      ${code}
    }
  `;
  
  const fn = new Function('sandbox', wrappedCode);
  return fn(sandboxGlobal);
}

// 测试
try {
  runInSandbox('return Math.PI * 2'); // 6.283185307179586
  runInSandbox('return document.cookie'); // 抛出 ReferenceError
} catch (e) {
  console.error(e.message); // "document" 在沙箱中不可用
}

⚠️ 警告: Proxy 方案的隔离级别有限。经验丰富的攻击者可以通过原型链逃逸(Prototype Escape)绕过 Proxy 拦截。例如 (function(){}).constructor('return this')() 可以获取真正的全局对象。

要防御原型链逃逸,需要额外处理:

// 防御原型链逃逸
function hardenSandbox(sandboxGlobal) {
  // 冻结所有内置对象的原型
  const prototypes = [
    Object.prototype, Function.prototype,
    Array.prototype, String.prototype,
    Number.prototype, Boolean.prototype,
    RegExp.prototype, Date.prototype,
    Promise.prototype, Map.prototype, Set.prototype,
  ];

  prototypes.forEach(proto => {
    Object.freeze(proto);
    // 阻止通过 __proto__ 访问
    Object.defineProperty(proto, '__proto__', {
      get() { throw new Error('原型链访问被禁止'); },
      configurable: false,
    });
  });

  // 阻止 Function 构造器
  const OriginalFunction = Function;
  const FakeFunction = function() {
    throw new Error('动态创建函数被禁止');
  };
  FakeFunction.prototype = OriginalFunction.prototype;
  FakeFunction.constructor = OriginalFunction;
  sandboxGlobal.Function = FakeFunction;
}

🔧 三、Node.js 端沙箱方案

3.1 vm 模块:V8 隔离上下文

Node.js 的 vm 模块提供了 createContextrunInContext 方法,可以在独立的 V8 上下文中执行代码。

const vm = require('vm');
const { createContext, runInContext } = vm;

// 创建隔离的上下文
const context = createContext({
  console: { log: console.log },
  Math, JSON, Array, Object, String, Number,
  setTimeout: (fn, ms) => setTimeout(fn, Math.min(ms, 5000)),
});

// 在沙箱中执行代码
function runInSandbox(code, timeout = 3000) {
  try {
    return runInContext(code, context, {
      timeout,                    // 执行超时(毫秒)
      displayErrors: true,        // 显示错误信息
      breakOnSigint: true,        // 支持 Ctrl+C 中断
    });
  } catch (e) {
    if (e.message.includes('timed out')) {
      throw new Error('代码执行超时,可能存在无限循环');
    }
    throw e;
  }
}

// 测试
console.log(runInSandbox('1 + 2 + 3')); // 6
console.log(runInSandbox('Math.sqrt(144)')); // 12

try {
  runInSandbox('while(true){}'); // 超时抛出错误
} catch (e) {
  console.error(e.message); // 代码执行超时,可能存在无限循环
}

⚠️ 警告: Node.js 官方文档明确指出 vm 模块不是安全的沙箱。逃逸方式包括:(function(){}).constructor('return process')() 可以获取 Node.js 的 process 对象。

3.2 vm2 / isolated-vm:更安全的 Node.js 沙箱

对于生产环境,推荐使用 isolated-vm 库,它基于 V8 的 Isolate 机制提供真正的进程级隔离。

// 安装:npm install isolated-vm
const ivm = require('isolated-vm');

async function runInIsolate(code, options = {}) {
  const {
    memoryLimit = 128,      // 内存限制(MB)
    timeout = 3000,          // 执行超时(ms)
    maxCpuTime = 1000,       // CPU 时间限制(ms)
  } = options;

  // 创建隔离的 V8 实例
  const isolate = new ivm.Isolate({ memoryLimit });
  const context = await isolate.createContext();

  // 注入安全的全局对象
  const jail = context.global;
  await jail.set('global', jail.derefInto());
  await jail.set('console', new ivm.Reference({
    log: (...args) => console.log('[sandbox]', ...args),
  }));

  try {
    const script = await isolate.compileScript(code);
    const result = await script.run(context, {
      timeout,
      promise: true,
    });
    return result;
  } finally {
    context.release();
    isolate.dispose();
  }
}

// 使用示例
const result = await runInIsolate(`
  let sum = 0;
  for (let i = 0; i < 1000; i++) sum += i;
  sum;
`, { memoryLimit: 64, timeout: 2000 });
console.log(result); // 499500

三种 Node.js 沙箱方案对比:

特性 vm 模块 vm2 isolated-vm
隔离级别 上下文级(弱) 进程级(中) V8 Isolate(强)
安全性 ❌ 可逃逸 ⚠️ 已知漏洞 ✅ 最安全
性能 最快 中等 较慢
内存限制 ❌ 无 ⚠️ 有限 ✅ 精确控制
CPU 限制 ✅ 超时 ✅ 超时 ✅ 精确控制
内存占用 最低 中等 较高
适用场景 可信代码 半可信代码 不受信任代码

💡 提示: vm2 在 2023 年已被标记为不再维护(deprecated),因为发现了多个无法修复的安全漏洞。新项目请直接使用 isolated-vm

💡 四、ShadowRealm API:JavaScript 的原生沙箱

4.1 ShadowRealm 是什么?

ShadowRealm 是 TC39 提案(Stage 3),旨在为 JavaScript 提供语言级别的隔离执行环境。它创建一个独立的全局对象和内置对象集合,代码在同一进程中运行但无法访问外部作用域。

// ShadowRealm 基本用法(需要浏览器或运行时支持)
const realm = new ShadowRealm();

// 在隔离环境中执行代码
const result = realm.evaluate('1 + 2 + 3');
console.log(result); // 6

// 导入模块
const getValue = realm.importValue('./my-module.js', 'getValue');
const value = await getValue();

4.2 ShadowRealm vs iframe vs Worker

// ShadowRealm 的核心优势:同步调用 + 真正的隔离
const realm = new ShadowRealm();

// ❌ iframe 和 postMessage 是异步的
// iframe.contentWindow.postMessage(code, '*');

// ✅ ShadowRealm 是同步的,性能更好
const add = realm.evaluate(`
  function(a, b) { return a + b; }
`);
console.log(add(1, 2)); // 3 - 同步返回
特性 ShadowRealm iframe Web Worker
执行模型 同步 异步(postMessage) 异步(postMessage)
全局对象 独立 独立 独立
DOM 访问
网络访问 可限制
模块导入 ✅ importValue ✅ importScripts
浏览器支持 Chrome 127+ 全部 全部
隔离级别 上下文级 浏览上下文级 线程级

⚠️ 警告: ShadowRealm 目前(2026年)仍处于 Stage 3,浏览器支持不完整。生产环境建议使用 iframe 方案作为主要选择。

📊 五、综合方案选择与最佳实践

5.1 方案选择决策树

根据你的使用场景选择合适的方案:

场景 推荐方案 原因
在线代码编辑器 iframe sandbox 最高隔离级别,支持 DOM
AI 生成代码执行 isolated-vm (Node) / iframe (浏览器) 需要最强安全性
插件系统 ShadowRealm / iframe 需要同步调用 + 隔离
数据转换管道 Web Worker 纯计算,无需 DOM
模板渲染引擎 Proxy + with 可信度较高,性能优先
单元测试运行器 vm + timeout 测试代码基本可信

5.2 安全检查清单

在部署任何沙箱方案之前,确认以下检查项:

  • 超时机制:所有代码执行都有超时限制(建议 3-5 秒)
  • 内存限制:限制可分配的最大内存(浏览器通过 iframe,Node 通过 isolated-vm)
  • API 白名单:只暴露必要的 API,其余全部屏蔽
  • 原型链保护:冻结 Object.prototypeFunction.prototype 等关键原型
  • 网络限制:禁止或限制 fetchXMLHttpRequestWebSocket
  • 错误隔离:沙箱内的错误不应影响主应用
  • 日志审计:记录沙箱内的所有 API 调用

5.3 实战:构建一个完整的在线代码执行器

综合以上方案,下面是一个面向生产环境的在线代码执行器:

// 生产级代码执行器
class CodeExecutor {
  constructor(options = {}) {
    this.timeout = options.timeout || 3000;
    this.memoryLimit = options.memoryLimit || 128;
    this.maxOutputSize = options.maxOutputSize || 10240;
  }

  // 浏览器端:使用 iframe 沙箱
  async executeInBrowser(code) {
    return new Promise((resolve, reject) => {
      const iframe = document.createElement('iframe');
      iframe.sandbox = 'allow-scripts';
      iframe.style.display = 'none';
      document.body.appendChild(iframe);

      const timer = setTimeout(() => {
        iframe.remove();
        reject(new Error('执行超时'));
      }, this.timeout);

      window.addEventListener('message', function handler(e) {
        if (e.source !== iframe.contentWindow) return;
        clearTimeout(timer);
        window.removeEventListener('message', handler);
        iframe.remove();
        
        if (e.data.error) {
          reject(new Error(e.data.error));
        } else {
          resolve(e.data.result);
        }
      });

      iframe.srcdoc = `
        <script>
          try {
            const logs = [];
            const fakeConsole = {
              log: (...a) => logs.push(a.map(String).join(' ')),
              warn: (...a) => logs.push('[warn] ' + a.join(' ')),
              error: (...a) => logs.push('[error] ' + a.join(' ')),
            };
            const fn = new Function('console', ${JSON.stringify(code)});
            const result = fn(fakeConsole);
            parent.postMessage({
              result: result,
              logs: logs
            }, '*');
          } catch(e) {
            parent.postMessage({ error: e.message }, '*');
          }
        <\/script>
      `;
    });
  }

  // Node.js 端:使用 isolated-vm
  async executeInNode(code) {
    const ivm = require('isolated-vm');
    const isolate = new ivm.Isolate({ memoryLimit: this.memoryLimit });
    const context = await isolate.createContext();

    try {
      const script = await isolate.compileScript(code);
      const result = await script.run(context, {
        timeout: this.timeout,
        promise: false,
      });
      return result;
    } finally {
      context.release();
      isolate.dispose();
    }
  }
}

// 使用
const executor = new CodeExecutor({ timeout: 2000 });

// 浏览器环境
if (typeof window !== 'undefined') {
  const result = await executor.executeInBrowser(`
    const data = [1, 2, 3, 4, 5];
    return data.reduce((a, b) => a + b, 0);
  `);
  console.log(result); // 15
}

⚠️ 六、常见陷阱与避坑指南

陷阱 1:以为 with 语句能完全隔离

// ❌ 错误:with 语句无法阻止原型链逃逸
with ({}) {
  // 可以通过构造器访问真正的 Function
  (function(){}).constructor('return this')();
  // 返回真正的 globalThis
}

陷阱 2:忽略正则表达式的 ReDoS 攻击

// ❌ 危险:恶意正则导致 CPU 100%
const maliciousRegex = /^(a+)+$/;
maliciousRegex.test('aaaaaaaaaaaaaaaaaaaaaaaaaaa!'); // 卡死

// ✅ 解决:限制正则执行时间,或使用 re2 引擎
const RE2 = require('re2');
const safeRegex = new RE2(maliciousRegex.source);

陷阱 3:忘记清理事件监听器

// ❌ 沙箱代码注册了全局事件监听器,即使 iframe 销毁也不会清理
iframe.srcdoc = `
  <script>
    window.addEventListener('message', (e) => {
      // 持续监听,即使 iframe 被移除
    });
  </script>
`;

// ✅ 正确做法:销毁 iframe 后清理
function destroySandbox(iframe) {
  iframe.src = 'about:blank'; // 先清空内容
  setTimeout(() => iframe.remove(), 100); // 延迟移除
}

🎯 总结

JavaScript 沙箱不是一个可以"一劳永逸"解决的问题。根据你的安全需求和性能要求,选择合适的方案:

  • 🔒 最高安全:iframe sandbox(浏览器)/ isolated-vm(Node.js)
  • 最高性能:vm 模块 + Proxy(仅限可信代码)
  • 🔮 未来方向:ShadowRealm API(等待标准成熟)

关键结论: 没有任何沙箱是 100% 安全的。沙箱只是纵深防御(Defense in Depth)的一层,还需要配合代码审查、静态分析、网络隔离等多层安全措施。

相关工具推荐:

  • isolated-vm — Node.js 的 V8 Isolate 沙箱
  • ses — Agoric 的 Secure EcmaScript 库
  • shadowrealm-polyfill — ShadowRealm 的 polyfill 实现
  • re2 — 安全的正则表达式引擎,防止 ReDoS

📚 相关文章