CVE-2026-43998
vm2: NodeVM require.root bypass via symlink traversal allows sandbox escape
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.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- High
- Privileges needed
- Low
- User interaction
- None
What an attacker can affect
- Confidentiality
- High
- Integrity
- High
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
8.5
vm2: NodeVM require.root bypass via symlink traversal allows sandbox escape
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.
- Record
CVE-2026-43998- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-59, CWE-59
- Updated
- 2026-11-02
EUVDEUVD-2026-30068
8.5
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.
- Record
EUVD-2026-30068- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-cp6g-6699-wx9c
8.5
vm2 has a NodeVM require.root bypass via symlink traversal that allows sandbox escape
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:
// 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():
// 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:
// 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:
// lib/resolver-compat.js:218
const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined;
Full Execution Chain
- Host creates
NodeVMwithrequire: { external: ['safe'], root: '/tmp/root', context: 'host' } - A symlink exists:
/tmp/root/node_modules/safe→/outside/root/vm2/(e.g., via pnpm, npm link, or workspaces) - Sandbox code calls
require('safe') DefaultResolver.resolveFull()resolves to/tmp/root/node_modules/safe/index.jstryFile()callsthis.fs.resolve(x)→path.resolve()→/tmp/root/node_modules/safe/index.js(symlink NOT followed) 6.…
- Record
GHSA-cp6g-6699-wx9c- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-59
- Updated
- 2026-11-02
NVD
8.5
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.
- Record
CVE-2026-43998- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-59
- Updated
- 2026-11-02
OSV
8.5
vm2: NodeVM require.root bypass via symlink traversal allows sandbox escape
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.
- Record
CVE-2026-43998- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-59
- Updated
- 2026-11-02
OSVGHSA-cp6g-6699-wx9c
8.5
vm2 has a NodeVM require.root bypass via symlink traversal that allows sandbox escape
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:
// 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():
// 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:
// 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:
// lib/resolver-compat.js:218
const checkedRootPaths = rootPaths ? (Array.isArray(rootPaths) ? rootPaths : [rootPaths]).map(f => fsOpt.resolve(f)) : undefined;
Full Execution Chain
- Host creates
NodeVMwithrequire: { external: ['safe'], root: '/tmp/root', context: 'host' } - A symlink exists:
/tmp/root/node_modules/safe→/outside/root/vm2/(e.g., via pnpm, npm link, or workspaces) - Sandbox code calls
require('safe') DefaultResolver.resolveFull()resolves to/tmp/root/node_modules/safe/index.jstryFile()callsthis.fs.resolve(x)→path.resolve()→/tmp/root/node_modules/safe/index.js(symlink NOT followed) 6.…
- Record
GHSA-cp6g-6699-wx9c- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H - Weakness
- CWE-59
- Updated
- 2026-11-02
OSVROOT-APP-NPM-CVE-2026-43998
8.5
CVE-2026-43998 in vm2 - Patched by Root
Root has patched CVE-2026-43998 in the vm2 package for Root:npm. Multiple fixed versions available.
- Record
ROOT-APP-NPM-CVE-2026-43998- CVSS 3.1
- 8.5 High
CVSS:3.1/AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H - Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
vm2npm |
3.10.5 to 3.10.5 | 3.11.0 |
vm2npm |
3.10.5 up to 3.11.0 | 3.11.0 |
vm2Root:npm |
before 3.10.5-aikido.6 | 3.10.5-aikido.63.10.5-aikido.73.10.5-aikido.8 |
@rootio/vm2Root:npm |
before 3.10.5-root.io.1 | 3.10.5-root.io.13.10.5-root.io.23.10.5-root.io.33.10.5-root.io.43.10.5-root.io.53.10.5-root.io.63.10.5-root.io.73.10.5-root.io.8 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-43998 is a high vulnerability (CVSS 8.5) in Patriksimek Vm2, published May 7, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.72%, the 54th percentile. Fixed in vm2 3.11.0.
References
github.com/patriksimek/vm2/security/advisories/GHSA-cp6g-6699-wx9c
access.redhat.com/security/cve/CVE-2026-43998
bugzilla.redhat.com/show_bug.cgi?id=2477206
security.access.redhat.com/data/csaf/v2/vex/2026/cve-2026-43998.json
access.redhat.com/errata/RHSA-2026:50850
github.com/patriksimek/vm2/releases/tag/v3.11.0
Left out here: the rest of 2 source descriptions. The JSON record has them.