Skip to content

CVE-2026-93606: Verified Reproduction

CVE-2026-93606: vm2 before 3.12.1 sandbox escape via host-realm Promise Symbol.species VM and NodeVM

CVE-2026-93606 is verified against patriksimek/vm2 · github. Affected versions: <= 3.12.0 (all versions 0 through 3.12.0 per VulnCheck affected range). Fixed in 3.12.1. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00383.

REPRO-2026-00383 patriksimek/vm2 · github Oct 9, 2026 CVE entry .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
45m 4s
Tool calls
159
Spend
$3.32
01 · Overview

What Is CVE-2026-93606?

CVE-2026-93606 is a critical-severity vulnerability affecting patriksimek/vm2 <= 3.12.0 (all versions 0 through 3.12.0 per VulnCheck affected range). Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00383).

02 · Severity & CVSS

CVE-2026-93606 Severity

CVE-2026-93606 is rated critical severity.

CRITICAL threat level
Weakness CWE-693

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected patriksimek/vm2 Versions

patriksimek/vm2 · github versions <= 3.12.0 (all versions 0 through 3.12.0 per VulnCheck affected range) are affected.

How to Reproduce CVE-2026-93606

$ pruva-verify REPRO-2026-00383
or curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00383/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-93606

sandbox escape — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

Sandboxed script run inside vm2 VM/NodeVM; script sets p.constructor[Symbol.species] on a host-realm Promise and calls p.then() with no onRejected

Attack chain
  1. vm.run()
  2. host Promise.prototype.then bridge apply-trap
  3. V8 PerformPromiseThen Thrower
  4. species-hijacked resultCapability.[[Reject]]
  5. raw host process object delivered to sandbox closure
  6. host child_process.execSync
How the agent worked 315 events · 159 tool calls · 43 min
43 minDuration
159Tool calls
55Reasoning steps
315Events
6Dead-ends
Agent activity over 43 min
Policy
1
Support
12
Repro
85
Judge
38
Variant
174
Verify
1
0:0042:54

Root Cause and Exploit Chain for CVE-2026-93606

Versions: ≤ 3.12.0 (all prior lines; the vulnerability is in the bridge's promise-rejection sanitizer introduced with the m283 defenses)
  • Affected versions: ≤ 3.12.0 (all prior lines; the vulnerability is in the bridge's promise-rejection sanitizer introduced with the m283 defenses)
  • Patched version: 3.12.1 (also current latest 3.12.2)
  • Risk: Critical (CVSS v4 10.0, GHSA-6454-5x88-m6jw). Any embedder that hands the sandbox a Promise-returning host API (caching layers, RPC stubs, fetch-like wrappers) exposes full host RCE: filesystem, child_process, env vars, outbound network.

Impact Parity

  • Disclosed/claimed maximum impact: sandbox escape → host arbitrary code execution.
  • Reproduced impact from this run: full sandbox escape with host command execution. From inside both VM and NodeVM, the sandboxed script (a) received the raw host process object as a live bridge proxy (isProxy: true), (b) read a host-only environment variable (HOST_ONLY_SECRET=CANARY123) invisible to the sandbox's own process stub, and (c) executed host shell commands via hostValue.mainModule.require('child_process').execSync, writing unique per-attempt marker files on the host filesystem.
  • Parity: full.

Root Cause

Two defense gaps compose:

  1. Species neutralization is sandbox-realm-only. lib/setup-sandbox.js overrides then/catch/finally on the sandbox intrinsic Promise.prototype to call resetPromiseSpecies(this) (GHSA-27g9-p43v-cw3v). A host Promise crossing the bridge keeps the host Promise.prototype methods, so this neutralization never runs for it. Meanwhile BaseHandler.set deliberately allows ordinary sandbox writes onto a non-frozen host object, so p.constructor = { [Symbol.species]: Evil } lands on the raw host promise.

  2. The rejection sanitizer only wraps function-valued slots. The bridge's apply-trap interception of host Promise.prototype.then/catch (normalizeHostPromiseCallbacks / makeSanitizedPromiseCallback in lib/bridge.js) wraps onFulfilled/onRejected only when the slot holds a function. Per PerformPromiseThen, a missing/non-callable onRejected makes V8 substitute its internal Thrower, which performs throw reason into resultCapability.[[Reject]] with the raw host value. Because resultCapability was built via SpeciesConstructor(p, %Promise%) → new Evil(GetCapabilitiesExecutor) — executed back in the sandbox through the proxy's [[Construct]] trap — [[Reject]] is an attacker sandbox closure. No handleException, ensureThis, or hostPromiseSanitizeReject chokepoint exists on this path.

Fix (vm2 3.12.1, GHSA-6454-5x88-m6jw): peelEffectivePromiseCall now returns the effective receiver of host then/catch/finally (also unwinding Reflect.apply), and neutralizeHostPromiseSpeciesOn installs constructor = undefined as an own data property on the raw host promise for the duration of the call, forcing SpeciesConstructor to fall back to the realm-correct host %Promise%. The reaction capability is then a genuine host promise; the raw settlement can only be observed by attaching a fresh .then/.catch, which re-enters the sanitizer. Verified: on 3.12.1 the sandbox script's hijack closure is never invoked (sandbox returned: UNSET) and no host marker file is created.

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; run twice consecutively — both runs exit 0).
  2. The script downloads the immutable npm tarballs vm2@3.12.0 (vulnerable) and vm2@3.12.1 (fixed), installs them with pinned integrity into the prepared project cache (/pruva/project-cache/vm2-pkgs, fallback bundle/artifacts/vm2-pkgs), generates bundle/repro/harness.js, and runs two clean attempts per build per sandbox class (VM ×2, NodeVM ×2 on each version) via node harness.js <vm2-dir> <VM|NodeVM> <id>, each invocation bounded by timeout 60.
  3. Expected evidence: every vulnerable attempt prints ESCAPE_CONFIRMED with {"isProxy":true,"envSecret":"CANARY123","exec":"PWNED_FROM_SANDBOX","markerWritten":true} and creates a host-side marker file repro/proof/marker-vulnerable-<mode>-<n>.txt containing the unique attempt token; every fixed attempt prints ESCAPE_NOT_CONFIRMED (sandbox returned: UNSET) and creates no marker.

Evidence

  • Per-attempt logs: bundle/repro/proof/{vulnerable,fixed}-{VM,NodeVM}-{1,2}.log
  • Host-written marker files (proof of host command execution from the sandbox): bundle/repro/proof/marker-vulnerable-VM-{1,2}.txt, bundle/repro/proof/marker-vulnerable-NodeVM-{1,2}.txt
  • Exploit harness executed: bundle/repro/harness.js
  • Diagnostics: bundle/logs/reproduction_steps.log
  • Machine-readable manifest with sha256 of every proof artifact and npm-tarball-bound target identity: bundle/repro/runtime_manifest.json
  • Key excerpt (vulnerable, VM): [VM:vulnerable-1] sandbox returned: {"isProxy":true,"envSecret":"CANARY123","exec":"PWNED_FROM_SANDBOX","markerWritten":true} → ESCAPE_CONFIRMED
  • Key excerpt (fixed, VM): [VM:fixed-1] sandbox returned: UNSET → ESCAPE_NOT_CONFIRMED
  • Environment: Node.js v24.18.0, linux x86_64; vm2@3.12.0 tarball sha256 263d59bfcdd5107915551b4181228fb5c8dd98f043faa78f2b8fb33f8fe8ffa8; vm2@3.12.1 tarball sha256 afa9d765ff89edcd8472ef2fb3e1707d2e9077aeffe8ea861fd46aa696726513.

Recommendations / Next Steps

  • Upgrade to vm2 ≥ 3.12.1 immediately; note vm2 is formally discontinued upstream, so prefer migrating to isolated-vm or Node's vm with out-of-process isolation for any untrusted-code workload.
  • The 3.12.1 fix (species neutralization at the SpeciesConstructor chokepoint + Reflect.apply peel) is the correct layered defense; regression tests should cover .then(), .catch(), .finally(), Function.prototype.call/apply, and Reflect.apply indirection against host promises.
  • Embedders should additionally avoid returning host promises that reject with host-pivotable values, and should freeze exposed host objects where feasible.

Additional Notes

  • Idempotent: the script reuses the cached package installs keyed by version, removes stale markers before each attempt, and was run twice consecutively with identical confirmed results.
  • The PoC mechanics were reconstructed from the vendor advisory GHSA-6454-5x88-m6jw and independently re-executed against the published npm artifacts in this run; no historical proof was reused.
  • Limitations: the escape requires an embedder-exposed host function returning a host-realm Promise (the documented vm2 pattern) and a rejection path; impact is unconditional once those hold, independent of what the promise rejects with.

CVE-2026-93606 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:006:53
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-93606 · REPRO-20
0:10
0:11
0:13
web search
0:25
0:27
web search
1:44
1:44
extract_facts
no facts extracted
1:47
1:47
supportclaim_contract
1:55
1:55
1:55
2:01
2:01
2:01
2:08
2:10
$ls -la /pruva/project-cache/ 2>/dev/null; node --version; npm --version; ls /pruva/project-cache/repo 2>/dev/null | head
2.8s✓
total 4
drwx------ 4 vscode vscode   80 Oct  9 13:08 .
drwxr-xr-x 3 root   root   4096 Oct  9 13:08 ..
drwx------ 2 vscode vscode   60 Oct  9 13:07 .pruva
drwx------ 2 vscode vscode   40 Oct  9 13:07 repo-mirrors
v24.18.0
11.16.0
2:26
2:28
web search
2:31
$npm view vm2 versions --json 2>&1 | tail -20; npm view vm2@3.12.1 dist.tarball 2>&1
3.5s✓
  "3.10.4",
  "3.10.5",
  "3.11.0",
  "3.11.1",
  "3.11.2",
  "3.11.3",
  "3.11.4",
  "3.11.5",
2:42
2:46
2:48
$mkdir -p /workspace/work/vm2-3.12.0 /workspace/work/vm2-3.12.1 && cd /workspace/work/vm2-3.12.0 && npm pack vm2@3.12.0 --silent && tar xzf vm2-3.12.0.tgz && cd /workspace/work/vm2-3.12.1 && npm pack vm2@3.12.1 --silent && tar xzf vm2-3.12.1.tgz && diff -ru /workspace/work/vm2-3.12.0/package /workspace/work/vm2-3.12.1/package | head -300
1.7s✓
{"cwd":"/workspace","duration_ms":1296,"exit_code":0,"stderr":"","stdout":"vm2-3.12.0.tgz\nvm2-3.12.1.tgz\ndiff -ru /workspace/work/vm2-3.12.0/package/lib/bridge.js /workspace/work/vm2-3.12.1/package/lib/bridge.js\n--- /workspace/work/vm2-3.12.0/package/lib/bridge.js\t1985-10-26 08:15:00.000000000 +0000\n+++ /workspace/work/vm2-3.12.1/package/lib/bridge.js\t1985-10-26 08:15:00.000000000 +0000\n@@ -70,6 +70,17 @@\n \n const OPNA = 'Operation not allowed on contextified object.';\n \n+// SECURITY (GHSA-j89j-5m6r-cr2q): cache THIS realm's global object at module\n+// init. bridge.js is loaded in both the host and the sandbox realm, so this\n+// constant resolves to the host global in the host copy and the sandbox global\n+// in the sandbox copy. Each bridge publishes it (as `result.global`) so the\n+// OTHER bridge can recognise — and refuse to deliver — the other realm's\n+// global object across the boundary. `global` (Node's global object binding)\n+// exists on every supported Node version, unlike `globalThis` (Node 12+), and\n+// is identical to the object V8's OrdinaryCallBindThis substitutes for a\n+// sloppy-mode function's nullish `this`.\n+const thisRealmGlobal = global;\n+\… [truncated]
4:20
4:21
$diff -ru /workspace/work/vm2-3.12.0/package /workspace/work/vm2-3.12.1/package | sed -n '300,600p'
0.6s✓
{"cwd":"/workspace","duration_ms":66,"exit_code":0,"stderr":"","stdout":"+\t\t\tdefined = otherReflectDefineProperty(obj, 'constructor', {\n+\t\t\t\t__proto__: null,\n+\t\t\t\tvalue: undefined,\n+\t\t\t\twritable: true,\n+\t\t\t\tenumerable: false,\n+\t\t\t\tconfigurable: true,\n+\t\t\t});\n+\t\t} catch (e) {\n+\t\t\tthrow thisFromOtherForThrow(e);\n+\t\t}\n+\t\tif (!defined) {\n+\t\t\tthrow new VMError('Unsafe host promise state; cannot neutralize species');\n+\t\t}\n+\n+\t\treturn {\n+\t\t\t__proto__: null,\n+\t\t\tobj: obj,\n+\t\t\toriginalDesc: originalDesc,\n+\t\t\tmarker: PROMISE_SPECIES_NEUTRALIZED,\n+\t\t};\n+\t}\n+\n+\t// SECURITY: restore the raw host promise's `constructor` after the guarded\n+\t// then/catch/finally call. Called from a `finally`; must not throw.\n+\tfunction restoreHostPromiseSpeciesOn(saved) {\n+\t\tif (!saved || saved.marker !== PROMISE_SPECIES_NEUTRALIZED) return;\n+\t\tconst obj = saved.obj;\n+\t\tconst originalDesc = saved.originalDesc;\n+\t\ttry {\n+\t\t\tif (originalDesc) {\n+\t\t\t\totherReflectDefineProperty(obj, 'constructor', originalDesc);\n+\t\t\t} else {\n+\t\t\t\totherReflectDeleteProperty(obj, 'constructor');\n+\t\t\t}\n+\t\t} catch (e) … [truncated]
4:23
6:53
08 · How to Fix

How to Fix CVE-2026-93606

Upgrade patriksimek/vm2 · github to 3.12.1 or later.

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-93606 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-93606

Is CVE-2026-93606 exploitable?

Yes. Pruva independently reproduced CVE-2026-93606 in patriksimek/vm2 and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00383).

How severe is CVE-2026-93606?

CVE-2026-93606 is rated critical severity.

What type of vulnerability is CVE-2026-93606?

CVE-2026-93606 is classified as CWE-693.

Which versions of patriksimek/vm2 are affected by CVE-2026-93606?

patriksimek/vm2 <= 3.12.0 (all versions 0 through 3.12.0 per VulnCheck affected range) is affected by CVE-2026-93606.

Is there a fix for CVE-2026-93606?

Yes. CVE-2026-93606 is fixed in patriksimek/vm2 3.12.1. Upgrading to the fixed version remediates the issue.

How can I reproduce CVE-2026-93606?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-93606 reproduction verified?

Yes. Pruva reproduced CVE-2026-93606 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-93606

Authoritative sources for CVE-2026-93606 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.