Proof/CVEs & advisories/CVE-2026-47698
The guard counted wrappers, not capabilities.
vm2 blocked a dangerous host prototype mutator when it was called directly, and again when it was wrapped once. JavaScript’s invocation machinery lets the same capability survive arbitrary wrapper composition.
.call indirectionFunction recoveredvm2’s VM is meant to run untrusted JavaScript without letting that code obtain raw host-realm capabilities. The relevant prerequisite is the one vm2 exists to handle: an application intentionally executes less-trusted JavaScript inside VM. This path needs no exposed require, no nesting, no host-module mocks and no network weakness.
A blocked host capability must remain blocked through every semantically equivalent invocation shape, not only direct and first-order wrapper forms.
vm2 mediates values crossing between sandbox and host through proxy handlers in lib/bridge.js. A core responsibility of that bridge is preventing sandbox code from invoking host functions that can mutate host object relationships.
Prior hardening recognized dangerous host prototype mutators and blocked direct invocation. Release 3.11.5 also tried to account for a single layer of call, apply or bind indirection:
if (isApplyIndirectionPrimitive(object)) {
let underlying;
try { underlying = otherFromThis(context); } catch (e) {}
if (isDangerousHostProtoMutator(underlying)) throw new VMError(OPNA);
}That logic inspects context for the one-layer form. In the stacked bypass, both the outer applied object and context are themselves Function.prototype.call; the dangerous setter has moved deeper into the argument list. The guard recognizes the wrapper but not the underlying capability.
The one-layer protection first appeared in commit 27c525f4615e2b983f122e2bed327d810126f5c8, released in 3.11.4 as hardening for an earlier issue. The stacked variant is why fixed-depth wrapper inspection is not a mediation strategy.
Sandbox code obtains a host prototype mutator
The public PoC uses the host Object.prototype.__proto__ getter/setter pair.
The setter is wrapped in nested call
Stacking the indirection moves the dangerous target past the depth the guard inspects.
A host error’s prototype chain is severed
The carrier is a host error produced through a WebAssembly streaming failure path.
The bridge maps an unexpected shape
A later exception crosses the membrane with a prototype shape the mapping logic does not anticipate.
constructor.constructor resolves to host Function
From there, code evaluates in the host realm rather than the sandbox realm.
Direct and one-layer
The dangerous mutator is refused when invoked directly and through a single wrapper — the guard is active.
Stacked indirection
The same capability reaches the host, corrupts the error prototype chain and yields the host Function constructor.
3.11.6
Conversion refuses to deliver dangerous host mutators; exception handling rejects foreign values with unexpected prototype roots.
The defense was syntactic. It recognized a dangerous function when called directly and when wrapped in a small number of anticipated ways — but the same function capability survives arbitrary wrapper composition.
A security membrane should reason about the underlying host capability reaching the sandbox, not about a fixed set of surface call shapes.
Equivalent capability, different syntax.
The productive hypothesis was not “find another dangerous function.” It was “take the function the guard already knows about, and change nothing except how it is invoked.”
Source mapping
Read the bridge’s mediation handlers and locate every dangerous-capability check.
Equivalence hypothesis
Treat a fixed-depth wrapper check as an invitation to add one more layer.
Runtime construction
Build the smallest chain that corrupts a host error and reaches a host constructor.
Skeptic gate
Confirm the single-layer form is refused, so the stacked form is the actual bypass.
Promotion and disclosure
Preserve shared reporter attribution and confirm the runtime dependency before publishing.
The public fix is commit a85acb61f81402c6eabf32760aa11272af6d0f9e, released in vm2 3.11.6. It adds controls at more fundamental boundaries rather than adding one more depth check:
- host-to-sandbox conversion helpers refuse to deliver dangerous host prototype mutators at the crossing point;
- exception handling rejects foreign host values whose prototype chain reaches
nullwithout traversing the sandbox’s expectedObject.prototypeboundary.
That blocks the capability where it crosses the membrane and hardens the corrupted-object path the public exploit chain relies on.
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:HRecovering the host Function constructor breaks vm2’s core isolation guarantee. Code an application intended to constrain to a sandbox can instead execute with the authority of the Node.js host process — files, environment variables, process credentials, network access and any other capability that process holds.
| Source | Affected | Fixed |
|---|---|---|
| GitHub Security Advisory | through 3.11.5 | 3.11.6 |
| CVE record | < 3.11.6 | 3.11.6 |
| Directly reviewed | 3.11.5, plus the 3.11.4 one-layer guard | 3.11.6 and the fixing change |
The wider historical range is retained as the publisher’s claim rather than release-by-release independent execution. The 3.11.4 hardening did not introduce the host-prototype mutation family; it added an incomplete guard. The first affected vm2 release was not independently established.
Upgrade to vm2 3.11.6 or later for this family, and check whether later vm2 advisories affect the intended deployment before relying on the sandbox at all.
For sandbox implementers, regression testing should cover semantic capability equivalence rather than a handful of syntax patterns:
- dangerous host functions stay blocked through nested
call,applyandbind; - the same holds through proxy wrappers and other function-forwarding constructions;
- exception-translation code rejects malformed or unexpectedly rooted host prototype chains instead of continuing to map a potentially corrupted object.
- GHSA-cfcw-xp6x-25gj — vm2 advisory and public PoC
- CVE-2026-47698 record
- vm2 repository and release history
Credit: The public advisory credits multiple reporters, including Charles Vosburgh. This page preserves that shared attribution and does not claim sole discovery. Research was AI-assisted through source mapping, hypothesis generation and release comparison; final validation and disclosure review remained human.