vm2 是一个沙箱,可以使用白名单中的 Node 内置模块运行不受信任的代码。
npm install vm2
## 快速示例```js
import { VM } from 'vm2';
const vm = new VM();
vm.run(`process.exit()`); // TypeError: process.exit is not a function
I need the actual content of chunk 5 to translate it. Please provide the Markdown text you'd like translated.```js import { NodeVM } from 'vm2';
const vm = new NodeVM({ require: { external: true, root: './', }, });
vm.run(
var request = require('request'); request('http://www.google.com', function (error, response, body) { console.error(error); if (!error && response.statusCode == 200) { console.log(body); // Show the HTML for the Google homepage. } });,
'vm.js',
);
## 重要安全免责声明
**在使用 vm2 之前,您应当了解其工作原理及其局限性。**
vm2 尝试在**与您的应用程序相同的 Node.js 进程内**对不受信任的 JavaScript 代码进行沙箱隔离。它通过一个复杂的 [Proxies](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 网络来实现这一点,这些代理会拦截并调解沙箱与宿主环境之间的每一次交互。
### 根本性挑战
JavaScript 是一种极其动态的语言。对象可以通过原型链访问,构造函数可以通过错误对象触达,符号提供了协议钩子,异步执行则创造了时序窗口。在 JavaScript 中,从一个对象遍历到另一个对象的方式数不胜数,这使得构建一个严丝合缝的进程内沙箱变得极其困难。
**我们对这一现实保持坦诚:** 尽管我们尽了最大努力,研究人员和安全专业人员仍在不断发现逃逸 vm2 沙箱的新方法。我们会在这些漏洞被报告后积极修补,但进程内沙箱的猫鼠游戏性质意味着:
1. **未来很可能会发现新的绕过方法。** 请查看我们的[安全公告](https://github.com/patriksimek/vm2/security/advisories)以了解已知漏洞。
2. **您必须保持 vm2 更新**,以便受益于最新的安全修复。请订阅安全公告并及时更新。
3. **vm2 不应成为您唯一的防线。** 在运行不受信任的代码时,纵深防御至关重要。
### 更稳健的替代方案
如果您需要更强的隔离保证,请考虑以下提供**真正的进程级或硬件级隔离**的替代方案:
| 解决方案 | 方法 | 性能 | 权衡 |
|----------|----------|-------------|------------|
| **[isolated-vm](https://github.com/laverdet/isolated-vm)** | 独立的 V8 隔离(不同的 V8 堆) | 快 | 处于维护模式;需要手动更新 V8 |
| **独立进程 / Worker** | `child_process` 或具有受限权限的 Worker 线程 | 中等 | IPC 开销更高;数据必须序列化 |
| **容器 / 虚拟机** | Docker、gVisor、Firecracker | 慢 | 启动开销;资源占用高 |
| **托管服务** | 基于云的代码执行(例如 AWS Lambda、Cloudflare Workers) | 可变 | 网络延迟;外部依赖 |
### vm2 可能仍然适用的场景
vm2 在以下情况下可能适用:
- 您需要与宿主对象紧密集成以及快速的同步通信
- 不受信任的代码来自相对可信的来源(例如内部工具、经过审核作者的插件系统)
- 您将 vm2 与其他安全层结合使用(网络隔离、文件系统限制、资源限制)
- 您接受风险并积极监控安全更新
**如果您运行的是来自完全不受信任来源的代码(例如任意用户提交),我们强烈建议使用具有更强隔离保证的解决方案。**
## 运行时
| 运行时 | 状态 |
|---------|--------|
| Node.js | 受支持。沙箱是一个安全边界。 |
| Bun | **实验性。** 部分功能兼容 — **不是**安全边界。 |
Bun 存在两个独立的限制,二者互不关联。
**它不是安全边界。** vm2 的威胁模型、[`docs/ATTACKS.md`](https://github.com/patriksimek/vm2/blob/main/docs/ATTACKS.md) 中的攻击目录以及 `test/ghsa/` 中的每一个回归测试都源自 V8 内部机制。Bun 所使用的 JavaScriptCore 有其自身的对应机制,且均未针对 vm2 的桥接层进行审计。测试套件在 Bun 下通过仅表明兼容性,并不代表沙箱在该环境下能够保持安全。**请勿在 Bun 上使用 vm2 来隔离不受信任的代码。**
**兼容性是部分的,而非对等的。** Bun 下运行通过仅覆盖了实际在该环境中执行的测试。`test/bun-skips.js` 列出了被排除的测试及其原因,已知的行为差异包括:
- `Buffer.from(arrayLike)` 返回一个零长度的缓冲区
- `VMScript` 的 `filename` / `lineOffset` / `columnOffset` 元数据不可观察,因为 JSC 的 CallSite 对象不携带任何方法
- 对带有不可配置访问器的冻结宿主对象执行 `Object.freeze` 会抛出代理不变式 `TypeError`,而 V8 不会
- 某些跨沙箱边界的 `Buffer` 操作速度大幅下降 — 在 Node 上 64 MB 的 `allocUnsafe` 仅需 1.7 秒,而在 Bun 上需要超过 400 秒,慢到足以被视为挂起
请将 Bun 支持视为针对可信代码的尽力而为的兼容性,并在依赖任何特定行为之前查看跳过列表。
## 功能特性
- 在单个进程中与您的代码并排安全地运行不受信任的代码
- 完全控制沙箱的控制台输出
- 沙箱对进程的方法具有受限访问权限
- 可以从沙箱中 require 模块(内置和外部)
- 您可以限制对某些(或全部)内置模块的访问
- 您可以在沙箱之间安全地调用方法并交换数据和回调
- 积极维护并修补已知的逃逸方法(请参阅[安全免责声明](#important-security-disclaimer))
- 支持转译器
## 工作原理
- 它使用内部的 VM 模块来创建安全上下文。
- 它使用 [Proxies](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Proxy) 来防止逃逸沙箱。
- 它覆盖内置的 require 以控制对模块的访问。
如需深入了解 vm2 的内部机制,请参阅 [docs/ATTACKS.md](https://github.com/patriksimek/vm2/blob/main/docs/ATTACKS.md)。
## Node 的 vm 与 vm2 有什么区别?
您可以亲自尝试:```js
import { runInNewContext } from "node:vm";
runInNewContext('this.constructor.constructor("return process")().exit()');
console.log('Never gets executed.');
请提供需要翻译的 Markdown 内容。```js import { VM } from 'vm2';
new VM().run('this.constructor.constructor("return process")().exit()'); // Throws ReferenceError: process is not defined
## 文档
- [VM](#vm)
- [NodeVM](#nodevm)
- [VMScript](#vmscript)
- [错误处理](#error-handling)
- [调试沙箱代码](#debugging-a-sandboxed-code)
- [只读对象](#read-only-objects-experimental)
- [受保护对象](#protected-objects-experimental)
- [跨沙箱关系](#cross-sandbox-relationships)
- [CLI](#cli)
- [2.x 到 3.x 的变更](https://github.com/patriksimek/vm2/wiki/2.x-to-3.x-changes)
- [1.x 和 2.x 文档](https://github.com/patriksimek/vm2/wiki/1.x-and-2.x-docs)
- [贡献指南](https://github.com/patriksimek/vm2/wiki/Contributing)
## VM
VM 是一个简单的沙箱,用于同步运行不受信任的代码,但不具备 `require` 功能。只有 JavaScript 内置对象和 Node 的 `Buffer` 可用。调度函数(`setInterval`、`setTimeout` 和 `setImmediate`)默认不可用。
**选项:**
- `timeout` - 脚本超时时间(毫秒)。**警告**:你可能希望将此选项与 `allowAsync=false` 一起使用。此外,对沙箱返回的对象进行操作可能会执行任意代码并绕过超时限制。应使用 `typeof` 测试返回对象是否为原始值,并在其他情况下完全丢弃该对象(使用此类对象进行日志记录或生成错误消息也可能再次执行任意代码)。
- `sandbox` - VM 的全局对象。
- `compiler` - `javascript`(默认)、`typescript`、`coffeescript` 或自定义编译器函数。如果该值设置为 `typescript` 或 `coffeescript`,库期望你已预先安装相应的编译器。**`typescript` 需要 `typescript@6` 或更早版本** — 请参阅 [编译器](#compilers)。
- `eval` - 如果设置为 `false`,任何对 `eval` 或函数构造函数(`Function`、`GeneratorFunction` 等)的调用都将抛出 `EvalError`(默认值:`true`)。
- `wasm` - 如果设置为 `false`,任何编译 WebAssembly 模块的尝试都将抛出 `WebAssembly.CompileError`(默认值:`true`)。注意:出于安全原因,`WebAssembly.JSTag` 已在沙箱内被移除,因此 wasm 代码无法捕获 JavaScript 异常。
- `allowAsync` - 如果设置为 `false`,任何使用 `async` 运行代码的尝试都将抛出 `VMError`(默认值:`true`)。
- `bufferAllocLimit` - 沙箱内部发起的单个 `Buffer.alloc` / `Buffer.allocUnsafe` / `Buffer.allocUnsafeSlow` / `Buffer(N)` / `new Buffer(N)` 请求的最大字节数。超过此上限的请求将同步抛出 `RangeError`,且不会执行宿主分配。默认值:`Infinity`(无上限,完全向后兼容)。在内存受限环境(Docker / Kubernetes / Lambda / serverless)中运行不受信任代码的嵌入者应选择有限上限(例如 `32 * 1024 * 1024`),作为分层 DoS 防御的一部分,就像选择 `timeout` 一样。请参阅下面的 [加固建议](#hardening-recommendations)。
**重要提示**:超时仅对通过 `run` 运行的同步代码有效。超时**不**适用于 VM 返回的任何方法。在某些情况下超时不起作用 - 请参阅 [#244](https://github.com/patriksimek/vm2/pull/244)。```js
import { VM } from 'vm2';
const vm = new VM({
timeout: 1000,
allowAsync: false,
sandbox: {},
});
vm.run('process.exit()'); // throws ReferenceError: process is not defined
你也可以从虚拟机中检索值。```js let number = vm.run('1337'); // returns 1337
**提示**:更多用法示例请参阅测试。
## NodeVM
与 `VM` 不同,`NodeVM` 允许你以与常规 Node 上下文中相同的方式 `require` 模块。
**选项:**
- `console` - `inherit` 用于启用控制台,`redirect` 用于将输出重定向到事件,`off` 用于禁用控制台(默认值:`inherit`)。
- `sandbox` - VM 的全局对象。
- `compiler` - `javascript`(默认值)、`typescript`、`coffeescript` 或自定义编译器函数(接收代码及其文件路径)。如果该值设置为 `typescript` 或 `coffeescript`,库期望你已预先安装相应的编译器。**`typescript` 需要 `typescript@6` 或更早版本** — 请参阅 [编译器](#compilers)。
- `eval` - 如果设置为 `false`,任何对 `eval` 或函数构造器(`Function`、`GeneratorFunction` 等)的调用都将抛出 `EvalError`(默认值:`true`)。
- `wasm` - 如果设置为 `false`,任何编译 WebAssembly 模块的尝试都将抛出 `WebAssembly.CompileError`(默认值:`true`)。注意:出于安全原因,`WebAssembly.JSTag` 已在沙箱内被移除,因此 wasm 代码无法捕获 JavaScript 异常。
- `bufferAllocLimit` - 与 `VM` 上的语义相同 — 沙箱内部单次 `Buffer.alloc` 系列请求的最大字节数。默认值:`Infinity`。请参阅 [加固建议](#hardening-recommendations)。
- `sourceExtensions` - 视为源代码的文件扩展名数组(默认值:`['js']`)。
- `require` - `true`、一个对象或一个 Resolver,用于启用 `require` 方法(默认值:`false`)。
- `require.external` - 值可以是 `true`、允许的外部模块数组,或一个对象(默认值:`false`)。所有匹配 `/node_modules/${any_allowed_external_module}/(?!/node_modules/)` 的路径都允许被 require。
- `require.external.modules` - 允许的外部模块数组。也支持通配符,例如指定 `['@scope/*-ver-??]` 将允许使用所有名称形式为 `@scope/something-ver-aa`、`@scope/other-ver-11` 等的模块。`*` 通配符不匹配路径分隔符。
- `require.external.transitive` - 布尔值,指示是否允许外部模块的传递依赖(默认值:`false`)。**警告**:当某个模块被传递性地 require 时,任何模块随后都可以正常 require 它,即使在该模块被加载之前这是不可能的。
- `require.builtin` - 允许的内置模块数组,接受 ["\*"] 表示全部(默认值:无)。**警告**:`"\*"` 可能很危险,因为可能会添加新的内置模块。
- `require.root` - 本地模块可以被 require 的受限路径(默认值:所有路径)。
- `require.mock` - 模拟模块的集合(外部或内置)。
- `require.context` - `host`(默认值)用于在宿主中 require 模块并将其代理到沙箱中。`sandbox` 用于在沙箱中加载、编译和 require 模块。`callback(moduleFilename, ext)` 用于按模块动态选择上下文。如果未指定,默认值为沙箱。除 `events` 外,内置模块始终在宿主中 require 并代理到沙箱中。
- `require.import` - 启动时要加载到 NodeVM 中的模块数组。
- `require.resolve` - 当模块在传统的 Node 查找路径中未找到时使用的额外查找函数。
- `require.customRequire` - 用于替代 `require` 函数从宿主加载模块。
- `require.strict` - `false` 表示不强制由 require 加载的模块使用严格模式(默认值:`true`)。
- `require.fs` - 自定义文件系统实现。
- `nesting` - **警告**:允许此选项存在安全风险,因为脚本可以创建能够 require 任何宿主模块的 NodeVM。`true` 用于启用 VM 嵌套(默认值:`false`)。
- `wrapper` - `commonjs`(默认值)用于将脚本包装到 CommonJS 包装器中,`none` 用于获取脚本返回的值。
- `argv` - 传递给 `process.argv` 的数组。
- `env` - 传递给 `process.env` 的对象。
- `strict` - `true` 表示加载的模块使用严格模式(默认值:`false`)。
**重要提示**:超时对 NodeVM 无效,因此它无法抵御 `while (true) {}` 或类似的恶意代码。
**请记住**:你允许的模块越多,你的沙箱就越脆弱。```js
import { NodeVM } from 'vm2';
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: {
external: true,
builtin: ['fs', 'path'],
root: './',
mock: {
fs: {
readFileSync: () => 'Nice try!',
},
},
},
});
// Sync
let functionInSandbox = vm.run('module.exports = function(who) { console.log("hello "+ who); }');
functionInSandbox('world');
// Async
let functionWithCallbackInSandbox = vm.run('module.exports = function(who, callback) { callback("hello "+ who); }');
functionWithCallbackInSandbox('world', greeting => {
console.log(greeting);
});
当 wrapper 设置为 none 时,NodeVM 对于同步代码的行为更类似于 VM。```js
assert.ok(vm.run('return true') === true);
**提示**:更多用法示例请参阅测试。
### 通过相对路径加载模块
要通过相对路径加载模块,如果脚本是字符串,则必须将正在运行的脚本的完整路径作为第二个参数传递给 vm 的 `run` 方法。该文件名随后会显示在脚本生成的任何堆栈跟踪中。```js
vm.run('require("foobar")', '/data/myvmscript.js');
如果正在运行的脚本是 VMScript,则路径在 VMScript 构造函数中给出。```js const script = new VMScript('require("foobar")', { filename: '/data/myvmscript.js' }); vm.run(script);
### Resolver
可以通过 `makeResolverFromLegacyOptions` 创建解析器,并用于多个 `NodeVM` 实例,从而共享已编译的模块代码,可能加快加载时间。第一个 `NodeVM` 示例可以使用 `makeResolverFromLegacyOptions` 重写如下。```js
const resolver = makeResolverFromLegacyOptions({
external: true,
builtin: ['fs', 'path'],
root: './',
mock: {
fs: {
readFileSync: () => 'Nice try!',
},
},
});
const vm = new NodeVM({
console: 'inherit',
sandbox: {},
require: resolver,
});
你可以通过使用预编译脚本来提升性能。预编译的 VMScript 可以多次运行。需要注意的是,代码并不绑定到任何 VM(上下文);相反,它会在每次运行前绑定,且仅针对该次运行生效。```js import { VM, VMScript } from 'vm2';
const vm = new VM(); const script = new VMScript('Math.random()'); console.log(vm.run(script)); console.log(vm.run(script));
它同时适用于 `VM` 和 `NodeVM`。```js
import { NodeVM, VMScript } from 'vm2';
const vm = new NodeVM();
const script = new VMScript('module.exports = Math.random()');
console.log(vm.run(script));
console.log(vm.run(script));
代码在首次运行时自动编译。也可以随时通过 script.compile() 编译代码。代码编译完成后,该方法不再有任何效果。
compiler 接受 javascript(默认)、typescript、coffeescript 或自定义函数。typescript 和 coffeescript 编译器是可选的——请自行安装相应包;vm2 不依赖其中任何一个。
内置的 typescript 编译器要求 typescript@6 或更早版本。
vm2 通过 TypeScript 的 transpileModule() API 进行转译。TypeScript 7 已将其从包入口点移除——在该版本中,require('typescript') 仅解析为 { version, versionMajorMinor },而替代 API 位于明确标记为不稳定的 typescript/unstable/* 子路径下,其中没有任何一个提供等效的单文件转译功能。因此,在 7.x 版本上,vm2 没有任何可回退的方案。
在安装了 TypeScript 7 的情况下选择 compiler: 'typescript',会在 new VMScript(...) / new VM(...) 时抛出异常:```
VMError: The installed TypeScript (7.0.2) does not expose the transpileModule() API that
vm2's built-in TypeScript compiler uses; it was removed from the package entry point in
TypeScript 7. Install typescript@6 or earlier, or pass your own transpiler as a function:
{ compiler: (code, filename) => javaScriptSource }.
要么固定使用 `typescript@6`,要么提供你自己的转译器——任何返回 JavaScript 的函数都可以,因此 TypeScript 7 的 `tsc`、esbuild、swc 或类型剥离器都是有效的:```js
import { VM, VMScript } from 'vm2';
import { transformSync } from 'esbuild';
const script = new VMScript('const x: number = 1; x', {
compiler: (code, filename) => transformSync(code, { loader: 'ts', format: 'cjs' }).code,
});
new VM().run(script);
自定义编译器接收 (code, filename) 并必须返回 JavaScript 源码。它在宿主 realm 中、沙箱化之前运行——请将其视为可信代码,切勿基于不可信输入构建编译器。
需要安装 coffee-script。使用 { header: false, bare: true } 进行编译;你传入的任何 compilerOptions 都会合并到这些选项之上。
代码编译和同步代码执行中的错误可以通过 try-catch 处理。异步代码执行中的错误可以通过为 Node 的 process 附加 uncaughtException 事件处理器来处理。```js
try {
var script = new VMScript('Math.random()').compile();
} catch (err) {
console.error('Failed to compile script.', err);
}
try { vm.run(script); } catch (err) { console.error('Failed to execute script.', err); }
process.on('uncaughtException', err => { console.error('Asynchronous error caught.', err); });
## 调试沙箱中的代码
你可以像调试普通进程中的代码一样,调试或检查在沙箱中运行的代码。
- 你可以使用断点(这要求你指定一个脚本文件名)
- 你可以使用 `debugger` 关键字。
- 你可以使用单步进入(step-in)来进入沙箱中运行的代码内部。
### 示例
/tmp/main.js:```js
import { VM, VMScript } from 'vm2';
import { readFileSync } from 'node:fs';
const file = `${__dirname}/sandbox.js`;
// By providing a file name as second argument you enable breakpoints
const script = new VMScript(readFileSync(file), file);
new VM().run(script);
/tmp/sandbox.js```js const foo = 'ahoj';
// The debugger keyword works just fine everywhere. // Even without specifying a file name to the VMScript object. debugger;
## 只读对象(实验性)
为防止沙盒脚本对代理对象添加、更改或删除属性,你可以使用 `freeze` 方法使对象变为只读。这仅在 VM 内部有效。冻结对象会进行深度处理。原始类型无法被冻结。
**未使用 `freeze` 的示例:**```js
const util = {
add: (a, b) => a + b,
};
const vm = new VM({
sandbox: { util },
});
vm.run('util.add = (a, b) => a - b');
console.log(util.add(1, 1)); // returns 0
使用 freeze 的示例:```js
const vm = new VM(); // Objects specified in the sandbox cannot be frozen.
vm.freeze(util, 'util'); // Second argument adds object to global.
vm.run('util.add = (a, b) => a - b'); // Fails silently when not in strict mode. console.log(util.add(1, 1)); // returns 2
**重要提示:** 无法冻结已经代理到 VM 中的对象。
## 受保护对象(实验性)
与 `freeze` 不同,此方法允许沙盒脚本在对象上添加、更改或删除属性,但有一个例外——无法附加函数。因此,沙盒脚本无法修改诸如 `toJSON`、`toString` 或 `inspect` 之类的方法。
**重要提示:** 无法保护已经代理到 VM 中的对象。
## 跨沙盒关系```js
const assert = require('assert');
const { VM } = require('vm2');
const sandbox = {
object: new Object(),
func: new Function(),
buffer: new Buffer([0x01, 0x05]),
};
const vm = new VM({ sandbox });
assert.ok(vm.run(`object`) === sandbox.object);
assert.ok(vm.run(`object instanceof Object`));
assert.ok(vm.run(`object`) instanceof Object);
assert.ok(vm.run(`object.__proto__ === Object.prototype`));
assert.ok(vm.run(`object`).__proto__ === Object.prototype);
assert.ok(vm.run(`func`) === sandbox.func);
assert.ok(vm.run(`func instanceof Function`));
assert.ok(vm.run(`func`) instanceof Function);
assert.ok(vm.run(`func.__proto__ === Function.prototype`));
assert.ok(vm.run(`func`).__proto__ === Function.prototype);
assert.ok(vm.run(`new func() instanceof func`));
assert.ok(vm.run(`new func()`) instanceof sandbox.func);
assert.ok(vm.run(`new func().__proto__ === func.prototype`));
assert.ok(vm.run(`new func()`).__proto__ === sandbox.func.prototype);
assert.ok(vm.run(`buffer`) === sandbox.buffer);
assert.ok(vm.run(`buffer instanceof Buffer`));
assert.ok(vm.run(`buffer`) instanceof Buffer);
assert.ok(vm.run(`buffer.__proto__ === Buffer.prototype`));
assert.ok(vm.run(`buffer`).__proto__ === Buffer.prototype);
assert.ok(vm.run(`buffer.slice(0, 1) instanceof Buffer`));
assert.ok(vm.run(`buffer.slice(0, 1)`) instanceof Buffer);
在命令行中使用 vm2 之前,请先通过 npm install vm2 -g 进行全局安装。```sh
vm2 ./script.js
## 加固建议
vm2 可以防止沙箱逃逸(即不可信代码获取宿主 realm 访问权限)。但它本身**并不能**阻止所有形式的资源耗尽或拒绝服务攻击。运行不可信代码的嵌入方应在沙箱周围增加以下分层防御措施。
### 1. 使用 `bufferAllocLimit` 限制内存分配
单次 `Buffer.alloc(N)` 调用(其中 `N` 由攻击者控制)会作为一次同步的宿主 C++ 分配执行,V8 的 `timeout` 无法中断该操作。在内存受限的环境中,约 100 字节的沙箱载荷即可导致宿主 RSS 飙升 100 MB 以上,并通过 OOM 使宿主进程崩溃。请设置 `bufferAllocLimit`(例如 `32 * 1024 * 1024`)以限制单次分配的大小:```js
const vm = new VM({
timeout: 1000,
bufferAllocLimit: 32 * 1024 * 1024,
allowAsync: false,
});
该上限同样适用于已弃用的 Buffer(N) 和 new Buffer(N) 路径。请注意,聚合性耗尽(大量小分配、Buffer.concat、Uint8Array、String.repeat、Array(n).fill() 等)不在此上限覆盖范围内——请结合主机端内存限制(--max-old-space-size、容器限制、cgroup)以实现全面覆盖。
unhandledRejection 处理器存在一类主机进程中止型 DoS:沙箱代码创建 async function、async function* 或 await using,其函数体抛出的值在堆栈格式化期间触发主机领域错误(例如 e.name = Symbol(); e.stack)。V8 通过该领域的固有 Promise 创建拒绝 promise,从而绕过 vm2 的 Promise 子类包装,使该拒绝以 unhandledRejection 形式逃逸到主机。在 Node 15+ 上,默认行为是终止进程。
要堵住此漏洞,需要改变可观察的主机行为,因此 vm2 默认不提供修复。嵌入方应安装一个进程级处理器,用于吞掉(或记录)源自沙箱的拒绝:```js // Recommended: filter rejections that originated inside vm2 and swallow them, // while letting your own host-side rejections propagate. process.on('unhandledRejection', (reason, promise) => { // Heuristic: rejections from the sandbox frequently surface as values // without proper Error semantics, or with stacks pointing at vm.js. // Adjust the predicate to match your application. if (looksLikeSandboxOrigin(reason)) { return; // swallow — don't terminate the process } // Otherwise: handle (or rethrow) as normal for your host code. yourLogger.error('unhandled rejection', reason); });
如果您的应用程序没有其他未处理拒绝的来源,那么全面吞并并记录日志是可以接受的:```js
process.on('unhandledRejection', reason => {
yourLogger.warn('swallowed sandbox rejection', reason);
});
针对此问题的定向修复可能会在未来的次要版本中通过可选的 swallowSandboxUnhandledRejections 标志发布;在此之前,宿主端处理器是推荐的缓解措施。
即使设置了 bufferAllocLimit,也请使用 --max-old-space-size(或等效的容器内存限制)运行宿主进程,并使其与工作负载相匹配。该上限可防范单次分配原语;操作系统级限制则可防范总体资源耗尽以及 vm2 尚未封堵的任何未来分配原语。
require.builtin: ['*'] 视为非沙箱配置'*' 通配符会扩展至大多数 Node 内置模块,包括 child_process、fs、dgram、net、http 和 dns。这些是完整的宿主能力原语——在 '*' 配置下,沙箱内可访问 require('child_process').execSync('id')。vm2 的 '*' 语义是有意为之(某些嵌入方会运行受信任但隔离的代码),但不应将其作为不受信任代码的默认配置。建议使用显式白名单,仅包含沙箱实际需要的最小模块集合。
nesting: true 是一个逃生舱口nesting: true 允许沙箱代码 require('vm2') 并构造嵌套的 NodeVM。嵌套 VM 的 require 配置由构造它的沙箱代码决定,而非受外层 VM 约束。 具体而言:```js
const vm = new NodeVM({ nesting: true, require: { builtin: [] } });
vm.run( const { NodeVM: NVM } = require('vm2'); // Inner VM's config is whatever the sandbox writes here: const inner = new NVM({ require: { builtin: ['child_process'] } }); inner.run('require("child_process").execSync("id")'); // RCE);
如果你设置了 `nesting: true`,实际上就等于授予了沙箱与你相同的信任级别。**不要对不受信任的代码启用 `nesting: true`。** 仅当你信任沙箱代码本身,但出于非安全原因需要 VM 式执行语义(全新的全局对象、受控超时)时才使用它。
`nesting: true` **需要一个显式的 `require` 配置对象**(例如 `require: { builtin: [] }` 或 `require: {}`)。任何其他形式——`require: false`、`require: undefined`、`require: null` 或完全省略 `require`——都会在构造时抛出 `VMError`(GHSA-m4wx-m65x-ghrr,取代 GHSA-8hg8-63c5-gwmx)。所有这些形式都会产生一个仅支持 NESTING_OVERRIDE 的解析器:沙箱可以 `require('vm2')` 但不能加载其他任何内容,这纯粹是一个逃逸原语,没有任何合法用途。要拒绝所有 require,请移除 `nesting: true`。要允许嵌套 VM,请提供显式的 `require` 配置,以便在调用点就能看到这种权衡。
## 已知问题
- 无法定义继承自代理类的类。这包括在 `Object.create` 中使用代理类。
- 直接 eval 不起作用。
- 记录沙箱数组时,属性中会重复显示数组部分。
- 源代码转换可能导致函数的源字符串不同。
- 存在从沙箱内部使 node 进程崩溃的方法。请参阅[加固建议](#hardening-recommendations)。
- 内置的 `typescript` 编译器无法与 TypeScript 7 或更高版本配合使用,因为该版本移除了 vm2 所使用的 `transpileModule()` API。请使用 `typescript@6` 或传入你自己的转译器——请参阅[编译器](#compilers)。
[npm-image]: https://img.shields.io/npm/v/vm2.svg
[npm-url]: https://www.npmjs.com/package/vm2
[license-image]: https://img.shields.io/npm/l/vm2.svg
[license-url]: https://raw.githubusercontent.com/patriksimek/vm2/resurrection/LICENSE.md
[downloads-image]: https://img.shields.io/npm/dm/vm2.svg
[downloads-url]: https://www.npmjs.com/package/vm2
[snyk-image]: https://snyk.io/test/github/patriksimek/vm2/badge.svg
[snyk-url]: https://snyk.io/test/github/patriksimek/vm2