Executive Summary
A critical authorization bypass (CVE-2026-100721) has been disclosed in vm2, the Node.js sandboxing library used by embedders to run untrusted JavaScript with restricted access to the host. The flaw sits in NodeVM's external-module resolver and allows sandboxed guest code to escape the sandbox and execute arbitrary code in the host process — even when the embedder explicitly allowlisted only a single, specific module.
CVSS Score: 9.0 (Critical) Fixed in: vm2 3.12.2
vm2 has a long history of sandbox-escape disclosures, and this CVE was published alongside several related vm2 advisories (CVE-2026-100722, CVE-2026-100723, CVE-2026-43999, CVE-2026-44007) in the same batch — underscoring that vm2's sandbox boundary should not be treated as sufficient for isolating genuinely untrusted code.
Vulnerability Overview
| Attribute | Value |
|---|---|
| CVE ID | CVE-2026-100721 |
| CVSS Score | 9.0 (Critical) |
| Type | Incorrect Authorization / Sandbox Escape |
| Component | NodeVM external-module resolver (lib/resolver-compat.js) |
| Attack Vector | Local (sandboxed code execution context) |
| Privileges Required | Ability to run guest code inside a NodeVM instance |
| User Interaction | None |
| Affected Versions | vm2 before 3.12.2 |
| Fixed Version | vm2 3.12.2 |
| Advisory | GHSA-5h3f-q97h-ccvc |
Technical Details
Root Cause
When an embedder configures require.external with a custom resolver (using context: 'host'), LegacyResolver.customResolve in lib/resolver-compat.js records the resolved module directory in this.externals as new RegExp('^' + escapeRegExp(resolvedPath)) — a prefix match with no path-separator or end-of-string boundary.
Attack Chain
1. Embedder allowlists a single external module, e.g. "foo", resolved
to a path like /app/node_modules/foo
2. Guest code inside the NodeVM calls require("foo") — this is
legitimately allowed and passes isPathAllowedForModule
3. Guest code then calls require() again, but targets the absolute
path of a sibling directory that merely shares the allowlisted
path as a string prefix, e.g. /app/node_modules/foo2/index.js
4. Because the allowlist check is a bare prefix match (no trailing
separator required), "/app/node_modules/foo2" passes the same
isPathAllowedForModule test as "/app/node_modules/foo"
5. The non-allowlisted sibling module is loaded through hostRequire()
BEFORE its exports are wrapped with vm.readonly protections
6. The sibling's top-level code executes directly in the host
process — full sandbox escape and arbitrary code executionNo special crafting of the target module's contents is required beyond it simply existing as a sibling directory — something an attacker with any ability to influence installed dependencies, or who already knows the host's node_modules layout, can arrange or exploit opportunistically.
Why CVSS 9.0?
A sandbox whose entire purpose is to contain untrusted code is defeated with a single crafted require() call, no additional primitives, and no user interaction — resulting in full code execution in the host context that the sandbox was meant to protect.
Related vm2 CVEs in This Disclosure Batch
| CVE | Issue | CVSS |
|---|---|---|
| CVE-2026-100721 | External-module resolver prefix-match bypass (this advisory) | 9.0 |
| CVE-2026-100722 | Improper host-side Promise rejection handling | 6.8 |
| CVE-2026-100723 | Buffer backing-store ownership leak via zlib.deflateSync | 7.5 |
| CVE-2026-43999 (GHSA-947f-4v7f-x2v8) | module builtin allowlist bypass exposes Module._load(), enabling RCE via child_process | — |
| CVE-2026-44007 (GHSA-8hg8-63c5-gwmx) | require('vm2') succeeds inside a nested NodeVM regardless of require: false, reaching child_process | — |
Applications relying on vm2 for isolation should treat this as a coordinated wave of sandbox-defeat techniques, not an isolated bug — patch to 3.12.2 to address all of the above.
Impact Assessment
| Impact Area | Description |
|---|---|
| Sandbox Escape | Guest code gains arbitrary code execution in the host Node.js process |
| Supply Chain Risk | Any product using vm2 to run third-party plugins, user scripts, or untrusted templates is exposed |
| Data Exposure | Host-level code execution grants access to environment variables, file system, and network from the sandboxed context |
| Chained Risk | Combined with the related CVEs in this batch, multiple independent escape paths exist even in restrictive configurations |
Recommendations
Immediate Actions
- Upgrade to vm2 3.12.2 or later immediately across all services embedding it.
- Audit
require.externalallowlist configurations — do not assume a directory-prefix allowlist is sufficient even after patching; validate with explicit path-boundary checks in your own embedding code where possible. - Inventory every service using vm2 for sandboxing untrusted code (plugins, user-submitted scripts, template engines, CI/CD job runners).
Longer-Term
- Reconsider vm2 for high-assurance isolation. Given the volume and severity of sandbox-escape CVEs disclosed against vm2, treat it as unsuitable for isolating genuinely hostile code; prefer OS-level isolation (containers, microVMs, separate processes with restricted privileges) for untrusted workloads.
- Monitor for anomalous child-process spawning or unexpected file/network access originating from sandboxed execution contexts.