CVE-2026-92957 · GHSA-8686-vhfx-7r3j
vm2: NodeVM node:-prefixed negative builtin deny bypass exposes child_process
- Risk score
- 73/100
- CVSS
- 9.9 v3.1
- Exploit chance (30 days)
- 0.5%
- Published
- 1 Oct 2026
Open in the live forecast Today's forecast
What to know
- What it is
- 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…
- Affected
- vm2 (npm) <= 3.11.6
- Fix
- Upgrade vm2 to 3.11.7 or later.
- Exploitation
- No known exploitation
Story map
At a glance
vm2: Red, act today. NodeVM's require.external option lets sandboxed code require() local files and npm packages. When require.external is enabled and require.root is not explicitly set to a path that excludes node_modules, two defaults combine to fully defeat the sandbox: require.root defaults to unrestricted — "if omitted every path is …
The flaws
- CVE-2026-92957: NodeVM node:-prefixed negative builtin deny bypass exposes child_process (CVSS 9.9 · EPSS 1%)
- CVE-2026-92946: NodeVM `require.external` without an explicit `require.root` grants unrestricted host filesystem access and full RCE (CVSS 10.0 · EPSS 1%)
- CVE-2026-92944: Vm2 sandbox escape on Node.js 26 through a stale PromiseThenLookupChain protector (CVSS 9.8 · EPSS 1%)
- CVE-2026-92956: Vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass (CVSS 10.0 · EPSS 1%)
The fix
Fix: Upgrade vm2 to 3.11.7 or later.
Who is exposed: vm2 (npm) <= 3.11.6
Where it stands
- Public proof of concept
Connects to
- Same weakness: Privilege escalation (CWE-269). 156 other warnings in 30 days
Why it is Amber
- CVSS 9.9 critical
- On your watchlist: npm
Affected packages
| Package | Ecosystem | Vulnerable | Fixed in |
|---|---|---|---|
| vm2 | npm | <= 3.11.6 | 3.11.7 |
References
- https://github.com/advisories/GHSA-8686-vhfx-7r3j
- https://github.com/patriksimek/vm2/security/advisories/GHSA-8686-vhfx-7r3j
- https://nvd.nist.gov/vuln/detail/CVE-2026-92957
- https://github.com/patriksimek/vm2/commit/b0f50662dd499ff33544bb42387958c64711af1e
- https://github.com/patriksimek/vm2/releases/tag/v3.11.7
- https://www.vulncheck.com/advisories/vm2-before-3.11.7-authentication-bypass-via-node-prefix
- https://github.com/advisories/GHSA-8686-vhfx-7r3j
Original 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.
Last activity 1 Oct 2026. Patch Forecast updates every hour.