Introduction
The vm2 Node.js sandboxing library's builtin denylist has been bypassed again, this time through two overlooked modules that give sandboxed code direct access to host process execution primitives. CVE-2026-47140 carries a CVSS 10.0 score. It is the fifth critical sandbox escape patched in the vm2 3.11.x release series during 2026, and that pattern calls into question whether denylist-based sandboxing is viable for running untrusted code.
vm2 is an open-source sandbox library for Node.js, maintained by Patrik Simek, with approximately 4,100 GitHub stars and broad adoption across the Node.js ecosystem. It runs untrusted JavaScript with controlled access to Node.js built-in modules and is positioned as a more secure alternative to the native vm module. It has become much more relevant as AI agent frameworks have adopted it to execute model-generated code in ostensibly isolated environments.
Technical information
Root cause: exact-match denylist with incomplete coverage
The vulnerability lives in lib/builtin.js, where NodeVM maintains a DANGEROUS_BUILTINS Set that determines which Node.js core modules are blocked from sandboxed code. Before the patch, this set contained module, worker_threads, cluster, vm, repl, inspector, trace_events, and wasi. The enforcement logic used DANGEROUS_BUILTINS.has(key), a simple exact string match against the requested module name.
This design fails in two ways:
Gap 1: process is entirely absent from the denylist. The process module was never added to DANGEROUS_BUILTINS. On Node.js 22 and later, the process object exposes getBuiltinModule(name), a function that can load any core module by name regardless of the embedder's allow or deny configuration. Sandboxed code that can require("process") can therefore call process.getBuiltinModule("child_process") and get a reference to child_process, a module that would normally be blocked. From there, execFileSync, exec, or spawn gives arbitrary OS command execution on the host.
Beyond getBuiltinModule(), the process module also exposes binding(), dlopen(), and the host process.env. Each one offers another route to escape the sandbox or exfiltrate sensitive data.
Gap 2: inspector/promises is not matched by the inspector denylist entry. While inspector is present in DANGEROUS_BUILTINS, the subpath builtin inspector/promises is a distinct module identifier. The exact match DANGEROUS_BUILTINS.has("inspector/promises") returns false because only the string "inspector" is in the Set. The inspector/promises module provides a Promise-based API to the V8 Inspector protocol. An attacker can instantiate a Session, connect it, and issue a Runtime.evaluate call to execute arbitrary JavaScript in the host process context, completely outside the sandbox boundary.
Vulnerability classification
CVE-2026-47140 is classified under CWE-693: Protection Mechanism Failure, which MITRE defines as occurring when "the product does not use or incorrectly uses a protection mechanism that provides sufficient defense against directed attacks against the product." The CVSS v3.1 vector is AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H, which means unauthenticated remote exploitation with low complexity, changed scope, and full impact on confidentiality, integrity, and availability. The CVSS v4.0 score from cybersecurity-help.cz is 9.3.
Attack flow
Exploitation requires that the target application's NodeVM instance has require enabled and that its require.builtin configuration includes process, inspector/promises, or the wildcard "*". The default NodeVM configuration (where require is disabled or no affected builtins are allowed) is not vulnerable.
Path 1: process.getBuiltinModule pivot
- The attacker supplies malicious JavaScript to be executed inside the vm2 sandbox.
- The code calls
require("process"), which succeeds becauseprocessis not in the denylist. - The code calls
.getBuiltinModule("child_process")on the returned process object and gets a reference to the host'schild_processmodule. - The code calls
.execFileSync("hostname")(or any other command), which executes on the host operating system. - The command output comes back to the sandboxed code, confirming full host command execution.
Path 2: inspector/promises Runtime.evaluate
- The attacker supplies malicious JavaScript to be executed inside the vm2 sandbox.
- The code calls
require("inspector/promises"), which succeeds because the exact string does not match theinspectordenylist entry. - The code creates a new
Session(), connects it, and calls.post("Runtime.evaluate", ...)with arbitrary JavaScript. - The V8 Inspector evaluates the JavaScript in the host realm, outside the sandbox.
The GitHub advisory documents a proof-of-concept file (dangerous-builtin-denylist-rce.js) that validated both paths on vm2 3.11.2 and Node.js v25.9.0.
The AI agent dimension
In May 2026, Microsoft Security Research framed the structural risk of vm2 sandbox escapes as "Prompts Become Shells." When AI agent frameworks use vm2 to execute code generated by language models, a prompt injection attack becomes a direct path to host RCE. The attacker does not need direct access to the application. They only need to influence the model's output so that it includes one of the exploit paths above. Kodem Security's analysis warns that the 2026 vm2 CVE wave turns AI agents into host RCE vectors.
Patch information
Maintainer Patrik Simek committed a public patch for CVE-2026-47140 on May 17, 2026 (commit a1ed47a), and it shipped in vm2 version 3.11.4. The fix lives entirely within lib/builtin.js and addresses the root cause with two structural changes.
1. process added to the denylist. The DANGEROUS_BUILTINS Set now includes process alongside the previously blocked names (module, worker_threads, cluster, vm, repl, inspector, trace_events, wasi). Sandboxed code can no longer load the raw host process module, which exposes getBuiltinModule(), binding(), dlopen(), and the host process.env.
2. Exact match replaced with family-prefix matching. A new helper function, isDangerousBuiltin(key), replaces every prior DANGEROUS_BUILTINS.has(key) call:
function isDangerousBuiltin(key) { if (typeof key !== 'string') return false; if (key.startsWith('node:')) key = key.slice(5); if (DANGEROUS_BUILTINS.has(key)) return true; const slash = key.indexOf('/'); if (slash > 0 && DANGEROUS_BUILTINS.has(key.slice(0, slash))) return true; return false; }
This function is the core of the fix. It first strips the optional node: URL-style prefix, so node:process and node:inspector/promises cannot evade the check through an alternate spelling. It then looks for a / character in the string. If it finds one, it takes everything before the slash (the "family name") and checks that against the denylist. As a result, inspector/promises, any future inspector/foo, and any hypothetical process/bar subpath builtin are blocked automatically, because their family name is in the dangerous set.
The function is enforced at two chokepoints inside lib/builtin.js.
The first is the BUILTIN_MODULES array construction, where Node's builtinModules list is filtered before the '*' wildcard can expand to them:
const BUILTIN_MODULES = (nmod.builtinModules || ...) .filter(s => !s.startsWith('internal/') && !isDangerousBuiltin(s));
The second is the addDefaultBuiltin() function, the gate that handles explicit allowlist entries (e.g., builtin: ['process']) and the lower-level makeBuiltins() API:
if (!special && isDangerousBuiltin(key)) return;
With both checks in place, dangerous builtins cannot leak into the sandbox, whether the embedder relies on the '*' wildcard expansion path or names a dangerous module explicitly. The SPECIAL_MODULES / mocks / overrides escape hatches are intentionally preserved, so embedders who genuinely need a stub can still register a sandbox-safe replacement.
The commit also adds a 175-line test suite (test/ghsa/GHSA-rp36-8xq3-r6c4/repro.js) that:
- validates all blocked spellings (
process,node:process,inspector/promises,node:inspector/promises) - confirms the
getBuiltinModulepivot tochild_processis blocked - confirms the
inspector/promisesSessionRuntime.evaluatepath is blocked - verifies the explicit allowlist path is also rejected
- includes a regression guard ensuring safe subpath builtins like
fs/promises,dns/promises, andstream/promisescontinue to load normally under'*'
To upgrade:
npm install [email protected]
Version 3.11.5, published on May 18, 2026, is the latest release and includes additional fixes.
Detection methods
Detecting exploitation of or exposure to CVE-2026-47140 takes several layers: dependency scanning, runtime behavioral monitoring, configuration auditing, and SIEM-based threat detection. Because this vulnerability operates entirely within the Node.js runtime (sandboxed JavaScript escaping to the host process), there are no traditional network-layer signatures or file-based indicators. Detection should focus on finding vulnerable installations, spotting the specific bypass patterns, and catching post-exploitation behavior.
Dependency and version scanning
The first thing to check is whether your environment runs a vulnerable version of the vm2 npm package. All versions through and including 3.11.3 are affected. Several SCA platforms have integrated this CVE:
- Snyk tracks it as SNYK-JS-VM2-17111172 and classifies it as "Incomplete List of Disallowed Inputs," with a CVSS v4.0 score of 9.4 and an EPSS probability of 0.13% (34th percentile).
- GitHub Advisory Database catalogs it under GHSA-rp36-8xq3-r6c4, which enables automatic Dependabot alerts and pull requests for repositories with a direct or transitive vm2 dependency.
- Endor Labs, Tenable, GitLab Advisory Database, and Vulners all independently index CVE-2026-47140.
For a quick local check, run npm ls vm2 in project directories to see whether any dependency tree includes a pre-3.11.4 release.
Configuration-level detection
Not every vm2 installation is exploitable by this specific CVE. The advisory explicitly states this is "not reachable with the default NodeVM configuration where require is disabled or no affected builtins are allowed." Exploitation requires that the NodeVM instance's require.builtin option either includes the wildcard "*" or explicitly allows process or inspector/promises. Auditing your NodeVM initialization code for these configurations is a targeted detection step and can help you prioritize remediation.
Runtime behavioral detection
Kodem Security's research on the 2026 vm2 sandbox escape wave lists behavioral indicators that apply directly to CVE-2026-47140 exploitation:
- Process indicators: Watch for
nodeprocesses spawning unexpected child processes, particularlychild_process.execorchild_process.spawncalls originating from contexts that should be sandboxed. Shell invocations (sh,bash,cmd.exe) parented to a Node.js runtime that should not be spawning them are strong indicators. - Filesystem indicators: Post-escape code commonly targets sensitive credentials. Monitor for reads of
~/.aws/credentials,~/.npmrc,~/.docker/config.json,/proc/self/environ, and any environment variable enumeration from within an agent or sandbox runtime. - Network indicators: Outbound DNS lookups or HTTPS connections from a sandboxed agent runtime to domains or IPs not on an application allowlist are suspicious, especially in AI agent frameworks and CI/CD runners where vm2 isolates LLM-generated code.
As Kodem notes, "no malware hashes, C2 domains, or named binaries are available for the 2026 vm2 CVE wave." These are vulnerability disclosures from researchers, not reports of observed campaigns with traditional IOCs.
SIEM and detection platform integration
The Clankerusecase threat-led detection library has cataloged CVE-2026-47140 and produces continuously updated detection queries for six platforms: Splunk SPL, Microsoft Sentinel KQL, Microsoft Defender KQL, Sigma rules, Datadog Cloud SIEM, and CrowdStrike Falcon LogScale. Each detection is mapped to MITRE ATT&CK techniques and kill chain phases. SOC teams can pull these queries into their existing SIEM environments.
Detection gaps
Kodem and Snyk both point out that conventional SCA tools detect that vm2 is installed and flag the CVE, but do not determine whether the runtime configuration actually exposes the vulnerable code path. An application using vm2 with restrictive require.builtin settings carries a very different risk profile than one using the wildcard "*". Closing this gap and prioritizing remediation accurately requires runtime application detection and response (ADR) tooling or manual configuration auditing.
Affected systems and versions
All versions of the vm2 npm package up to and including 3.11.3 are affected. The vulnerability is fully patched in version 3.11.4.
The vulnerability is only exploitable when the NodeVM configuration meets one of these conditions:
requireis enabled andrequire.builtinincludes the wildcard"*"requireis enabled andrequire.builtinexplicitly includesprocessrequireis enabled andrequire.builtinexplicitly includesinspector/promises
The default NodeVM configuration, where require is disabled or no affected builtins are allowed, is not vulnerable to this specific CVE.
The proof of concept was validated on vm2 3.11.2 running on Node.js v25.9.0. The process.getBuiltinModule() exploit path requires Node.js 22 or later, the version that introduced that API.
Vendor security history
vm2 has a documented, recurring pattern of critical sandbox escape vulnerabilities, all classified as CWE-693:
| Year | CVE | CVSS | Bypass mechanism | Patched version |
|---|---|---|---|---|
| 2023 | CVE-2023-32314 | Critical | Sandbox escape | N/A |
| 2026 | CVE-2026-22709 | 10.0 | Promise callback bypass | 3.11.0 |
| 2026 | CVE-2026-24781 | 10.0 | inspect function breakout | 3.11.0 |
| 2026 | CVE-2026-26956 | 10.0 | Sandbox escape (confirmed on 3.10.4) | 3.11.x |
| 2026 | CVE-2026-44000 | 10.0 | Sandbox escape RCE | 3.11.x |
| 2026 | CVE-2026-47140 | 10.0 | process/inspector/promises denylist bypass | 3.11.4 |
Kodem Security reported a total of 13 vm2 sandbox escape vulnerabilities in the 2026 wave. Five CVSS 10.0 escapes patched across the 3.11.x series suggest that the structural weakness is the denylist-based sandbox model itself, not any individual implementation oversight.
The maintainer has been responsive and releases patches as drop-in replacements with no API changes. The commit for CVE-2026-47140 goes beyond adding individual entries. Its family-prefix matching via isDangerousBuiltin() is a structural improvement, which suggests an attempt to address the root-cause pattern. Under the current architecture, though, any future Node.js builtin or subpath not covered by the DANGEROUS_BUILTINS family set will create a new escape vector.
References
- NVD: CVE-2026-47140
- GitHub Security Advisory: GHSA-rp36-8xq3-r6c4
- Patch Commit a1ed47a
- vm2 Release v3.11.4
- OSV: GHSA-rp36-8xq3-r6c4
- Snyk: SNYK-JS-VM2-17111172
- Endor Labs: CVE-2026-47140
- Vulners: CVE-2026-47140
- cybersecurity-help.cz: VU131734
- Kodem Security: vm2 Sandbox Escape, AI Agent RCE Risk
- Semgrep: New Sandbox Escape Affecting Popular Node.js Library
- Endor Labs: CVE-2026-22709 Analysis
- BeyondMachines: Critical Sandbox Escapes in vm2
- GitHub Advisory Database: GHSA-rp36-8xq3-r6c4
- Clankerusecase Detection Library
- vm2 GitHub Repository
- CWE-693: Protection Mechanism Failure



