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 注入:通过
innerHTML、eval等方式注入恶意脚本
沙箱的核心目标是实现三层隔离:
| 隔离层 | 目标 | 技术手段 |
|---|---|---|
| 代码隔离 | 不同代码的执行上下文互不干扰 | 独立的全局对象、作用域链 |
| 资源隔离 | 限制 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 属性是浏览器原生提供的最强隔离机制。它创建一个完全独立的浏览上下文,有独立的 window、document、全局对象。
<!-- 基础 iframe 沙箱配置 -->
<iframe
sandbox="allow-scripts"
srcdoc="<script>console.log('isolated!')</script>"
></iframe>
sandbox 属性的可选值:
| 属性值 | 作用 | 安全性 |
|---|---|---|
| (无) | 最严格:禁止一切 | ✅ 最安全 |
allow-scripts |
允许执行脚本 | ✅ 安全 |
allow-same-origin |
允许访问父页面存储 | ❌ 危险 |
allow-forms |
允许提交表单 | ⚠️ 中等 |
allow-popups |
允许弹出窗口 | ⚠️ 中等 |
⚠️ 警告: 绝对不要同时使用
allow-scripts和allow-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 提供了独立的线程环境,天然没有 document、window 等 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 模块提供了 createContext 和 runInContext 方法,可以在独立的 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.prototype、Function.prototype等关键原型 - ✅ 网络限制:禁止或限制
fetch、XMLHttpRequest、WebSocket - ✅ 错误隔离:沙箱内的错误不应影响主应用
- ✅ 日志审计:记录沙箱内的所有 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