Coordinated disclosureCVE-2026-47698 · GHSA-cfcw-xp6x-25gjvm2 · npm9.8 Critical

Proof/CVEs & advisories/CVE-2026-47698

CVE.09 — Published record

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.

CVECVE-2026-47698
AdvisoryGHSA-cfcw-xp6x-25gj
Packagevm2
Ecosystemnpm · Node.js
Affected≤ 3.11.5
Fixed3.11.6
Severity9.8 Critical
CreditOne of several reporters
A blocked host capability must stay blocked through every semantically equivalent invocation shape — not only the shapes the guard anticipated.
Weakness
CWE-913
Trigger
stacked .call indirection
Control
one-layer guard active
Outcome
host Function recovered
Shared attribution.The public advisory credits multiple reporters. This page preserves that shared credit and does not claim sole discovery. The proof of concept is already public in GHSA-cfcw-xp6x-25gj and is not republished here.

vm2’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:

One-layer guardvm2 3.11.5
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.

1

Sandbox code obtains a host prototype mutator

The public PoC uses the host Object.prototype.__proto__ getter/setter pair.

2

The setter is wrapped in nested call

Stacking the indirection moves the dangerous target past the depth the guard inspects.

3

A host error’s prototype chain is severed

The carrier is a host error produced through a WebAssembly streaming failure path.

4

The bridge maps an unexpected shape

A later exception crosses the membrane with a prototype shape the mapping logic does not anticipate.

5

constructor.constructor resolves to host Function

From there, code evaluates in the host realm rather than the sandbox realm.

Runtime dependency. The chain needs a compatible host-error carrier. Retained validation reproduced the relevant error path on multiple modern Node.js releases; that detail should not be generalized to every Node version without testing.
Negative control

Direct and one-layer

The dangerous mutator is refused when invoked directly and through a single wrapper — the guard is active.

Vulnerable path

Stacked indirection

The same capability reaches the host, corrupts the error prototype chain and yields the host Function constructor.

Fixed control

3.11.6

Conversion refuses to deliver dangerous host mutators; exception handling rejects foreign values with unexpected prototype roots.

CONTROL execution begins inside new VM().run(...) CONTROL require / nesting / host modules not used GUARD single-layer wrapper is NOT the bypass STACKED nested Function.prototype.call defeats depth check HOST error prototype chain severed RECOVERED constructor is a host-realm Function: CONFIRMED MARKER=VM2_HOST_REALM_ESCAPE

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.
The SecHive research loop

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.”

SK.01

Source mapping

Read the bridge’s mediation handlers and locate every dangerous-capability check.

SK.02

Equivalence hypothesis

Treat a fixed-depth wrapper check as an invitation to add one more layer.

SK.03

Runtime construction

Build the smallest chain that corrupts a host error and reaches a host constructor.

SK.04

Skeptic gate

Confirm the single-layer form is refused, so the stacked form is the actual bypass.

HUMAN

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:

  1. host-to-sandbox conversion helpers refuse to deliver dangerous host prototype mutators at the crossing point;
  2. exception handling rejects foreign host values whose prototype chain reaches null without traversing the sandbox’s expected Object.prototype boundary.

That blocks the capability where it crosses the membrane and hardens the corrupted-object path the public exploit chain relies on.

Critical
Published advisory severity
9.8
CVSS 3.1
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H

Recovering 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.

SourceAffectedFixed
GitHub Security Advisorythrough 3.11.53.11.6
CVE record< 3.11.63.11.6
Directly reviewed3.11.5, plus the 3.11.4 one-layer guard3.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:

  1. dangerous host functions stay blocked through nested call, apply and bind;
  2. the same holds through proxy wrappers and other function-forwarding constructions;
  3. exception-translation code rejects malformed or unexpectedly rooted host prototype chains instead of continuing to map a potentially corrupted object.
2026-08-14
Public GHSA and PoC published.
AUGUST 2026
vm2 3.11.6 published with the fixing changes.
2026-08-26
Public source, release boundaries and retained controls re-reviewed.
  1. GHSA-cfcw-xp6x-25gj — vm2 advisory and public PoC
  2. CVE-2026-47698 record
  3. 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.