Skip to content

GHSA-jxxv-8r27-vm4p CVE-2026-92950

vm2 CLI provides no sandbox isolation - host-realm require() is reachable from sandboxed scripts

Critical npm
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

  1. Source - bin/vm2:3 → lib/cli.js:7-18. process.argv[2] is the attacker-authored script path. The CLI invokes:
    NodeVM.file(path, { verbose: true, require: { external: true } });
    
    Without require.root, require.context, nor require.builtin.
  2. Hop - lib/nodevm.js:618-636. NodeVM.file reads the file and calls new NodeVM(options).run(body, resolvedFilename).
  3. Hop - lib/nodevm.js:335 → lib/resolver-compat.js:205-266 (makeResolverFromLegacyOptions). Destructures external:true, rootPaths=undefined, hostRequire=defaultRequire (line 218), context='host' (default, line 219). Because typeof externalOpt !== 'object' (line 265) it returns a CustomResolver with checkedRootPaths=undefined and pathContext = () => 'host' (line 263).
  4. Hop - lib/setup-node-sandbox.js:86-123 (requireImpl). Sandbox require(id) resolves via resolver.resolve(...) (lib/nodevm.js:380-383). lib/resolver.js:244-275 handles absolute/relative specifiers; tryFile at lib/resolver.js:327-329 gates on this.isPathAllowed(x).
  5. Barrier (gap) - lib/resolver-compat.js:53-54:
    isPathAllowed(filename) {
        if (this.rootPaths === undefined) return true;
    
    With no root configured, every filesystem path is allowed. checkAccess (lib/resolver.js:39-42) delegates to the same method.
  6. 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);
    }
    
    hostRequire is defaultRequire (lib/resolver-compat.js:20-23) - the real host require(). The required module's top-level body executes in the host realm before vm.readonly() wraps the exports; wrapping happens too late to constrain side-effects. loadNode (lib/resolver-compat.js:80-83) is identical for .node native addons (process.dlopen in 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

PackageVulnerableFixed 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

Timeline

  1. Published
  2. Last updated by the source
  3. Latest EPSS score
AdvisorySeverityWhy 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
Is this in your code?

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