JavaScript 沙箱技术实践指南
JavaScript 沙箱技术实践指南
最近在做微前端项目的时候,遇到了一个棘手的问题:如何安全地执行第三方代码?传统的 iframe 和 eval 方案都有各种问题,于是深入研究了一下 JavaScript 的沙箱技术,踩了不少坑,在这里分享一下经验。
目录
遇到的问题
做微前端的时候,需要动态加载第三方模块,但是直接 eval 太危险了:
// 这种情况经常遇到
window.userInfo = { name: 'admin', token: 'secret' };
// 第三方代码可能这样搞你
function maliciousCode() {
// 偷取用户信息
fetch('/steal-data', {
method: 'POST',
body: JSON.stringify(window.userInfo),
});
// 或者直接清空本地存储
localStorage.clear();
sessionStorage.clear();
}
传统方案的坑
iframe 的各种问题
刚开始想用 iframe 搞定:
<iframe src="about:blank" sandbox="allow-scripts"></iframe>
结果发现问题一堆:
- 每个 iframe 都是个 DOM 节点,创建多了页面卡得要死
- 通信要用 postMessage,写起来贼麻烦
- CSS 样式隔离做不好,经常样式乱套
- 移动端还有各种坑,某些浏览器支持不好
// iframe 通信真的很烦
// 主页面
iframe.contentWindow.postMessage(
{
type: 'execute',
code: userCode,
},
'*'
);
// iframe 里面
window.addEventListener('message', (e) => {
if (e.origin !== window.location.origin) return; // 还得验证来源
const { type, code } = e.data;
if (type === 'execute') {
try {
const result = eval(code); // 这不还是要用 eval 吗?
parent.postMessage({ type: 'result', data: result }, '*');
} catch (error) {
parent.postMessage({ type: 'error', message: error.message }, '*');
}
}
});
eval 更危险
// 直接 eval 就是在作死
function runUserCode(code) {
return eval(code); // 用户可以执行任意代码
}
// 包装一下也没用
function betterEval(code) {
return new Function(code)(); // 还是能访问全局变量
}
// 比如用户输入这个:
const maliciousCode = `
// 可以访问所有全局变量
console.log(window.userToken);
// 可以修改原型链
Array.prototype.hack = function() { /* 搞破坏 */ };
// 可以发送网络请求
fetch('/admin/delete-all-data', { method: 'POST' });
`;
所以必须找更好的方案。
ShadowRealm - 新的希望
ShadowRealm 是个很有前景的新 API,虽然还在提案阶段,但已经能解决很多问题了。
基本用法
// 创建一个完全隔离的环境
const realm = new ShadowRealm();
// 在里面执行代码
const result = realm.evaluate(`
const data = { message: '这里是隔离环境' };
JSON.stringify(data);
`);
console.log(result); // '{"message":"这里是隔离环境"}'
这样执行的代码完全拿不到外面的全局变量,很安全。
实际项目中怎么用
我封装了一个类:
class SafeCodeRunner {
constructor() {
this.realm = new ShadowRealm();
this.setupEnvironment();
}
setupEnvironment() {
// 在沙箱里预置一些安全的工具函数
this.realm.evaluate(`
globalThis.utils = {
add: (a, b) => a + b,
multiply: (a, b) => a * b,
// 字符串处理,防止 XSS
escape: (str) => str.replace(/[<>&"]/g, (m) => ({
'<': '<', '>': '>', '&': '&', '"': '"'
})[m])
};
`);
}
runCode(code) {
try {
return this.realm.evaluate(`
(function() {
${code}
})();
`);
} catch (error) {
console.error('代码执行失败:', error);
return null;
}
}
}
// 使用起来很简单
const runner = new SafeCodeRunner();
const result = runner.runCode(`
const x = 10;
const y = 20;
return utils.add(x, y); // 只能用预置的工具函数
`);
console.log(result); // 30
模块加载
ShadowRealm 还能动态加载模块,这对微前端很有用:
const realm = new ShadowRealm();
// 加载外部模块
const mathUtils = await realm.importValue('./math-utils.js', 'calculate');
const result = mathUtils(5, 3);
// 或者直接在沙箱里 import
realm.evaluate(`
import { processData } from './data-processor.js';
globalThis.processor = processData;
`);
不过目前支持还不够好,Chrome 需要开启实验性功能。
用 Web Workers 做沙箱
Web Workers 天然隔离,没有 DOM 访问权限,用来做沙箱挺好的。我之前在项目里就是用的这个方案。
简单版本
class WorkerSandbox {
constructor() {
this.worker = null;
this.messageId = 0;
this.callbacks = new Map();
}
init() {
// 动态创建 Worker 代码
const workerCode = `
self.addEventListener('message', (e) => {
const { id, code, data } = e.data;
try {
// 在 Worker 里执行用户代码
const func = new Function('data', code);
const result = func(data);
self.postMessage({ id, success: true, result });
} catch (error) {
self.postMessage({
id,
success: false,
error: error.message
});
}
});
`;
const blob = new Blob([workerCode], { type: 'application/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
// 监听返回结果
this.worker.addEventListener('message', (e) => {
const { id, success, result, error } = e.data;
const callback = this.callbacks.get(id);
if (callback) {
this.callbacks.delete(id);
if (success) {
callback.resolve(result);
} else {
callback.reject(new Error(error));
}
}
});
}
async run(code, data = {}) {
return new Promise((resolve, reject) => {
const id = ++this.messageId;
this.callbacks.set(id, { resolve, reject });
this.worker.postMessage({ id, code, data });
// 超时处理
setTimeout(() => {
if (this.callbacks.has(id)) {
this.callbacks.delete(id);
reject(new Error('执行超时'));
}
}, 5000);
});
}
destroy() {
if (this.worker) {
this.worker.terminate();
this.worker = null;
}
}
}
// 用起来很简单
const sandbox = new WorkerSandbox();
sandbox.init();
// 执行用户代码
const result = await sandbox.run(
`
// 这里的代码在 Worker 中运行,完全隔离
const numbers = data.list;
return numbers.map(n => n * 2);
`,
{
list: [1, 2, 3, 4, 5],
}
);
console.log(result); // [2, 4, 6, 8, 10]
生产环境版本
实际项目中还要考虑更多问题,比如内存限制、API 白名单等:
class ProductionWorkerSandbox {
constructor(options = {}) {
this.config = {
timeout: 5000,
memoryLimit: 10 * 1024 * 1024, // 10MB,防止内存爆炸
allowedAPIs: ['console', 'Math', 'JSON'], // 白名单
...options,
};
this.worker = null;
}
createWorkerScript() {
const { allowedAPIs, memoryLimit } = this.config;
return `
// 清理危险的全局对象
delete self.importScripts;
delete self.XMLHttpRequest;
delete self.fetch;
// 只保留白名单 API
const allowedGlobals = ${JSON.stringify(allowedAPIs)};
const safeGlobal = {};
allowedGlobals.forEach(api => {
if (self[api]) {
safeGlobal[api] = self[api];
}
});
// 简单的内存监控
let memoryUsage = 0;
const memoryLimit = ${memoryLimit};
// 重写 Array 构造函数来监控内存
const OriginalArray = Array;
Array = function(...args) {
const estimatedSize = args.length * 8;
if (memoryUsage + estimatedSize > memoryLimit) {
throw new Error('内存使用超限,可能存在内存泄漏');
}
memoryUsage += estimatedSize;
return new OriginalArray(...args);
};
self.addEventListener('message', (e) => {
const { id, code, data, timeout } = e.data;
const timer = setTimeout(() => {
self.postMessage({
id,
success: false,
error: '代码执行超时,可能存在死循环'
});
}, timeout);
try {
// 使用 with 限制作用域
const func = new Function('data', 'globals', \`
with (globals) {
\${code}
}
\`);
const result = func(data, safeGlobal);
clearTimeout(timer);
self.postMessage({ id, success: true, result });
} catch (error) {
clearTimeout(timer);
self.postMessage({
id,
success: false,
error: error.message
});
}
});
`;
}
async init() {
const script = this.createWorkerScript();
const blob = new Blob([script], { type: 'application/javascript' });
this.worker = new Worker(URL.createObjectURL(blob));
this.messageId = 0;
this.callbacks = new Map();
this.worker.addEventListener('message', (e) => {
const { id, success, result, error } = e.data;
const callback = this.callbacks.get(id);
if (callback) {
this.callbacks.delete(id);
if (success) {
callback.resolve(result);
} else {
callback.reject(new Error(error));
}
}
});
}
async run(code, data = {}) {
return new Promise((resolve, reject) => {
const id = ++this.messageId;
this.callbacks.set(id, { resolve, reject });
this.worker.postMessage({
id,
code,
data,
timeout: this.config.timeout,
});
});
}
}
Proxy 沙箱实现
Proxy 沙箱的思路就是拦截全局对象的访问,只允许访问白名单里的 API。这种方案比较轻量,性能也不错。
class ProxySandbox {
constructor(whitelist = []) {
this.allowedAPIs = new Set([
'console',
'Math',
'JSON',
'Array',
'Object',
'String',
'Number',
'Boolean',
'Date',
'RegExp',
...whitelist,
]);
this.sandbox = this.createSandbox();
}
createSandbox() {
const sandbox = Object.create(null);
// 复制白名单中的 API
this.allowedAPIs.forEach((api) => {
if (typeof window[api] !== 'undefined') {
sandbox[api] = window[api];
}
});
return new Proxy(sandbox, {
get(target, prop) {
if (prop in target) {
return target[prop];
}
// 拒绝访问未授权的属性
console.warn(`尝试访问被禁止的API: ${prop}`);
return undefined;
},
set(target, prop, value) {
// 只允许设置用户自定义的属性
if (typeof prop === 'string' && !prop.startsWith('__')) {
target[prop] = value;
return true;
}
console.warn(`尝试设置被禁止的属性: ${prop}`);
return false;
},
});
}
run(code) {
const func = new Function('sandbox', `with (sandbox) { ${code} }`);
try {
return func(this.sandbox);
} catch (error) {
console.error('代码执行出错:', error);
throw error;
}
}
}
// 使用示例
const sandbox = new ProxySandbox(['fetch']); // 允许网络请求
const result = sandbox.run(`
const data = { message: 'Hello World' };
console.log('沙箱日志:', data.message); // 这个可以
// window.alert('弹窗'); // 这个会被拦截
return JSON.stringify(data);
`);
console.log(result); // '{"message":"Hello World"}'
不过 Proxy 沙箱有个问题,就是 with 语句在严格模式下不能用,而且性能不是特别好。
Node.js 下的 VM 模块
如果是在 Node.js 环境下,用 VM 模块是最好的选择,隔离性很强。
const vm = require('vm');
class NodeSandbox {
constructor(options = {}) {
this.config = {
timeout: 5000,
displayErrors: false,
...options,
};
this.context = this.createContext();
}
createContext() {
const sandbox = {
// 提供安全的 console
console: {
log: (...args) => console.log('[沙箱]', ...args),
error: (...args) => console.error('[沙箱]', ...args),
warn: (...args) => console.warn('[沙箱]', ...args),
},
// 基础 API
Math,
JSON,
Array,
Object,
String,
Number,
Boolean,
Date,
RegExp,
// 安全的定时器
setTimeout: (fn, delay) => {
return setTimeout(() => {
try {
fn();
} catch (error) {
console.error('定时器执行错误:', error);
}
}, delay);
},
};
return vm.createContext(sandbox);
}
run(code) {
try {
return vm.runInContext(code, this.context, {
timeout: this.config.timeout,
displayErrors: this.config.displayErrors,
});
} catch (error) {
console.error('VM 执行失败:', error);
throw error;
}
}
async runAsync(code) {
const wrappedCode = `
(async function() {
${code}
})()
`;
try {
return await vm.runInContext(wrappedCode, this.context, {
timeout: this.config.timeout,
});
} catch (error) {
console.error('异步代码执行失败:', error);
throw error;
}
}
}
// 使用示例
const sandbox = new NodeSandbox();
const result = sandbox.run(`
const factorial = (n) => n <= 1 ? 1 : n * factorial(n - 1);
factorial(5);
`);
console.log(result); // 120
// 异步代码示例
const asyncResult = await sandbox.runAsync(`
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
await delay(1000);
return '异步执行完成';
`);
console.log(asyncResult); // '异步执行完成'
VM 的好处是隔离很彻底,而且可以设置内存限制、CPU 时间限制等,适合服务端使用。
实际项目中的应用
说了这么多理论,来看看实际怎么用。我在项目中主要用在这几个场景:
1. 在线代码运行器
做过一个类似 CodePen 的在线代码编辑器,用 Worker 沙箱来执行用户代码:
class CodeRunner {
constructor() {
this.sandbox = new WorkerSandbox();
this.sandbox.init();
}
async runCode(code, input = '') {
// 重定向 console 输出
const runnerCode = `
let output = '';
const originalConsole = console.log;
console.log = (...args) => {
output += args.join(' ') + '\\n';
};
try {
${code}
return {
success: true,
output: output.trim(),
error: null
};
} catch (error) {
return {
success: false,
output: output.trim(),
error: error.message
};
}
`;
return await this.sandbox.run(runnerCode, { input });
}
}
// 前端调用
const runner = new CodeRunner();
const result = await runner.runCode(`
console.log('Hello World!');
const add = (a, b) => a + b;
console.log('结果:', add(3, 4));
`);
console.log(result);
// { success: true, output: 'Hello World!\n结果: 7', error: null }
2. 微前端插件系统
在微前端项目里,不同团队开发的模块需要隔离:
class MicroAppManager {
constructor() {
this.apps = new Map();
this.sandbox = new ProxySandbox(['fetch', 'localStorage']);
}
loadApp(name, code) {
try {
const app = this.sandbox.run(`
(function() {
${code}
// 每个微应用必须导出这些方法
if (typeof microApp === 'undefined') {
throw new Error('微应用必须导出 microApp 对象');
}
if (!microApp.mount || !microApp.unmount) {
throw new Error('微应用必须实现 mount 和 unmount 方法');
}
return microApp;
})()
`);
this.apps.set(name, app);
return true;
} catch (error) {
console.error(`加载微应用 ${name} 失败:`, error);
return false;
}
}
mountApp(name, container) {
const app = this.apps.get(name);
if (!app) {
throw new Error(`微应用 ${name} 不存在`);
}
return app.mount(container);
}
unmountApp(name) {
const app = this.apps.get(name);
if (app && app.unmount) {
app.unmount();
}
}
}
// 微应用代码示例
const appCode = `
const microApp = {
name: 'UserCenter',
version: '1.2.0',
mount(container) {
container.innerHTML = '<div>用户中心模块加载成功</div>';
console.log('用户中心模块已挂载');
return Promise.resolve();
},
unmount() {
console.log('用户中心模块已卸载');
return Promise.resolve();
}
};
`;
const manager = new MicroAppManager();
manager.loadApp('userCenter', appCode);
manager.mountApp('userCenter', document.getElementById('app-container'));
3. 表达式计算器
之前做过一个报表系统,需要支持用户自定义计算公式:
class FormulaEngine {
constructor() {
this.builtinFunctions = {
// 数学函数
abs: Math.abs,
ceil: Math.ceil,
floor: Math.floor,
round: Math.round,
max: Math.max,
min: Math.min,
// 字符串函数
len: (str) => String(str).length,
upper: (str) => String(str).toUpperCase(),
lower: (str) => String(str).toLowerCase(),
// 日期函数
today: () => new Date().toISOString().split('T')[0],
now: () => new Date().toISOString(),
};
}
calculate(formula, variables = {}) {
// 先检查公式安全性
if (!this.isFormulaSafe(formula)) {
throw new Error('公式包含不安全的代码');
}
const context = {
...this.builtinFunctions,
...variables,
};
const code = `
with (context) {
return (${formula});
}
`;
try {
const func = new Function('context', code);
return func(context);
} catch (error) {
throw new Error(`公式计算错误: ${error.message}`);
}
}
isFormulaSafe(formula) {
// 检查危险关键字
const dangerous = [
/\beval\b/,
/\bFunction\b/,
/\bsetTimeout\b/,
/\bsetInterval\b/,
/\bwindow\b/,
/\bdocument\b/,
];
return !dangerous.some((pattern) => pattern.test(formula));
}
}
// 实际使用
const engine = new FormulaEngine();
// 简单计算
console.log(engine.calculate('2 + 3 * 4')); // 14
// 使用内置函数
console.log(engine.calculate('max(10, 20, 5) + abs(-15)')); // 35
// 使用变量 (报表场景)
const reportData = {
销售额: 10000,
成本: 6000,
税率: 0.1,
};
const profit = engine.calculate('(销售额 - 成本) * (1 - 税率)', reportData);
console.log('净利润:', profit); // 3600
// 字符串处理
const formula = 'upper("hello") + " " + len("world")';
console.log(engine.calculate(formula)); // "HELLO 5"
用户可以在界面上输入公式,系统安全地执行计算,既灵活又安全。
性能测试
写了个简单的性能测试,对比不同沙箱方案:
class PerformanceTest {
constructor() {
this.testCode = `
const fibonacci = (n) => n <= 1 ? n : fibonacci(n-1) + fibonacci(n-2);
return fibonacci(20); // 计算斐波那契数列第20项
`;
}
async testWorkerSandbox(rounds = 50) {
const sandbox = new WorkerSandbox();
await sandbox.init();
const start = performance.now();
const promises = [];
for (let i = 0; i < rounds; i++) {
promises.push(sandbox.run(this.testCode));
}
await Promise.all(promises);
const end = performance.now();
sandbox.destroy();
return {
method: 'Worker 沙箱',
totalTime: end - start,
avgTime: (end - start) / rounds,
throughput: Math.round((rounds / (end - start)) * 1000),
};
}
async testProxySandbox(rounds = 50) {
const sandbox = new ProxySandbox();
const start = performance.now();
for (let i = 0; i < rounds; i++) {
sandbox.run(this.testCode);
}
const end = performance.now();
return {
method: 'Proxy 沙箱',
totalTime: end - start,
avgTime: (end - start) / rounds,
throughput: Math.round((rounds / (end - start)) * 1000),
};
}
async testDirectExecution(rounds = 50) {
const start = performance.now();
for (let i = 0; i < rounds; i++) {
const func = new Function(this.testCode);
func();
}
const end = performance.now();
return {
method: '直接执行 (不安全)',
totalTime: end - start,
avgTime: (end - start) / rounds,
throughput: Math.round((rounds / (end - start)) * 1000),
};
}
async runAllTests() {
console.log('开始性能测试...\n');
const workerResult = await this.testWorkerSandbox();
const proxyResult = await this.testProxySandbox();
const directResult = await this.testDirectExecution();
console.table([directResult, proxyResult, workerResult]);
console.log('\n结论:');
console.log('- 直接执行最快,但不安全');
console.log('- Proxy 沙箱中等性能,轻量级隔离');
console.log('- Worker 沙箱较慢,但隔离最彻底');
}
}
// 运行测试
const test = new PerformanceTest();
test.runAllTests();
/*
典型结果 (仅供参考):
┌─────────┬─────────────────────┬───────────┬─────────┬────────────┐
│ (index) │ method │ totalTime │ avgTime │ throughput │
├─────────┼─────────────────────┼───────────┼─────────┼────────────┤
│ 0 │ '直接执行 (不安全)' │ 45.2 │ 0.9 │ 1106 │
│ 1 │ 'Proxy 沙箱' │ 89.6 │ 1.8 │ 558 │
│ 2 │ 'Worker 沙箱' │ 2847.3 │ 56.9 │ 18 │
└─────────┴─────────────────────┴───────────┴─────────┴────────────┘
*/
从测试结果可以看出:
- 直接执行:性能最好,但完全不安全
- Proxy 沙箱:性能损失约 50%,但隔离较弱
- Worker 沙箱:性能损失很大,但隔离最强
选择时要根据实际场景权衡安全性和性能。
兼容性踩坑
| 特性 | Chrome | Firefox | Safari | Edge | Node.js |
|---|---|---|---|---|---|
| Web Workers | ✅ | ✅ | ✅ | ✅ | ❌ |
| ShadowRealm | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 |
| Proxy | ✅ | ✅ | ✅ | ✅ | ✅ |
| VM 模块 | ❌ | ❌ | ❌ | ❌ | ✅ |
| Compartments | 🚧 | 🚧 | 🚧 | 🚧 | 🚧 |
Polyfill 方案
// ShadowRealm Polyfill 示例
if (typeof ShadowRealm === 'undefined') {
global.ShadowRealm = class ShadowRealmPolyfill {
constructor() {
this.worker = new Worker(/* worker code */);
this.messageId = 0;
this.pending = new Map();
}
evaluate(code) {
return new Promise((resolve, reject) => {
const id = ++this.messageId;
this.pending.set(id, { resolve, reject });
this.worker.postMessage({ id, code });
});
}
async importValue(specifier, name) {
const code = `
import * as module from '${specifier}';
module.${name};
`;
return await this.evaluate(code);
}
};
}
总结
新一代 JavaScript 隔离沙箱技术为我们提供了更安全、高效的代码隔离解决方案:
技术选择建议
- 生产环境推荐: Web Workers + Proxy 组合
- Node.js 环境: VM 模块
- 未来投资: 关注 ShadowRealm 发展
- 高性能需求: 沙箱池化管理
核心优势
- ✅ 真正的代码隔离
- ✅ 更好的性能表现
- ✅ 细粒度权限控制
- ✅ 现代化 API 设计
- ✅ 更强的安全保障
应用场景
- 在线代码编辑器
- 插件系统
- 微前端架构
- 表达式引擎
- 动态脚本执行
总的来说,JavaScript 沙箱技术正在快速发展,我们终于有了比 iframe 和 eval 更好的选择。
写于 2025 年 9 月,基于实际项目经验总结
更多推荐


所有评论(0)