vm2 coordinated dump (~12 GHSAs): incomplete-fix family and default-config escapes¶
Wave in one line: Coordinated dump: incomplete-fix family, default-config escapes, and engine-scheduled callback bypasses (Oct 5 22:3xZ)
Source: hourly offensive-security scan of GitHub Security Advisories, 2026-10-05. Between 22:33Z and 23:00Z, ~12 vm2 advisories with sequential CVE IDs (CVE-2026-92933 through CVE-2026-92959, plus CVE-2026-100723) were published simultaneously — a coordinated single-audit dump against the semi-abandoned patriksimek/vm2 fork. Five carry CVSS 10.0. This is the single largest sandbox-escape publication event tracked on this wiki and it is durable for two reasons: (1) every product that uses vm2 as its "user code is safe here" boundary is now a fingerprinted target class, and (2) the dump is a public worked example of how to audit a realm bridge — each advisory names the earlier GHSA it bypasses, so the fix-lineage map is the attack-surface enumeration. This page complements the May 29 vm2/SGLang page; several of this dump's legs are second-generation bypasses of advisories covered there.
Advisories (all vm2 ≤ 3.11.6 / current HEAD):
- GHSA-wjwh-qqvp-g4p4 / CVE-2026-92956 — WebAssembly.compileStreaming Promise-species escape (10.0)
- GHSA-fcqc-726x-5wfc / CVE-2026-92947 — shared Buffer-pool host memory read/write (10.0)
- GHSA-j3hm-6rg5-mchv / CVE-2026-92946 —
require.externalwithoutrequire.root= unrestricted host access (10.0) - GHSA-3vgf-8m4q-q4qr / CVE-2026-92953 — host TypedArray/ArrayBuffer intrinsics still mutable (10.0)
- GHSA-88hf-g992-jg85 / CVE-2026-92955 — NodeVM
console:'inherit'→ EventEmitter.prototype.emit RCE (10.0) - GHSA-489w-w794-jq94 / CVE-2026-100723 — zlib builtin returns pooled host Buffer (10.0)
- GHSA-x965-fc75-jpqh / CVE-2026-92934 — AggregateError cycle revisit returns raw host proxy (9.0)
- GHSA-gjq8-xm47-88rc / CVE-2026-92954 — host Promise rejection terminates host process (8.6)
- GHSA-f8gf-w286-fmq2 / CVE-2026-92959 —
allowAsync:falsebypass via Promise static thenable assimilation (7.1) - GHSA-r4fx-v8hh-22mv / CVE-2026-92942 —
timeoutbypass via FinalizationRegistry GC callback (7.5) - GHSA-r273-hxvj-fxhp / CVE-2026-92933 —
util.getCallSites()bypasses host-frame redaction (5.8) - GHSA-x6m4-chr9-cg97 / CVE-2026-92936 — host-realm SyntaxError stack leaks absolute host paths (5.8)
Authorized validation only
Prove escapes only inside a lab build of the product that embeds vm2, or an explicitly authorized target tenant with a disposable sandbox. Benign markers only: write a canary file in a disposable temp dir, read a synthetic env var you set, or prove read/write against a buffer you allocated. Never read real secrets, never run against production tenants, never weaponize the shared-Buffer leg beyond a self-allocated pool slice.
1. Default configurations are the attack surface, not exotic flags¶
Three of the 10.0 legs require nothing unsafe in the embedder config:
- README quick-start is vulnerable (CVE-2026-92946):
NodeVM({require:{external:true, root:'./'}}— the documented example — defaultsrequire.contextto"host"and leaves unlisted paths unrestricted;./node_modules/vm2sits insideroot:'./', so sandbox code requires vm2's own package, constructs a fresh unrestricted nestedNodeVM, and reacheschild_process. Rule: when a sandbox option's safe value is "omit the parameter", every example that omits it is a finding. console:'inherit'is the default (CVE-2026-92955):console._stdout/_stderrprototype chain runs throughEventEmitter.prototype, and nothing blocks sandbox writes to it. OverwriteEventEmitter.prototype.emit, makeprocessemit any event, and your function runs withthis === processon the host. It also defeats--disallow-code-generation-from-strings, because the escape never touches the host Function constructor. Rule: enumerate inherited-prototype chains of every default-exposed host object, not just explicitly injected ones. Note the advisory's own framing: the host__proto__getter reachable from sandbox has been reported in four prior advisories and was still never patched when this one landed.- Default
new VM()on Node 26 is escapable (CVE-2026-92956): the prior fix removedWebAssembly.promising/Suspending(JSPI), butWebAssembly.compileStreaming/instantiateStreamingstill produce a raw host-realm Promise on Node 26; controllingSymbol.speciesthroughPromise.prototype.finallycaptures the host error, and from the host error's constructor you walk to the hostFunctionand realprocess. Rule: newer Node runtimes re-arm old escape classes. Fingerprint the host Node version beside the sandbox version, and after any Node upgrade assume prior "engine API not exposed" mitigations have lapsed.
2. Incomplete-fix genealogy is the recon map¶
Six legs are explicitly bypasses of named earlier fixes: 92956 bypasses GHSA-6j2x-vhqr-qr7q (the May 29 page's JSPI fix), 92934 bypasses GHSA-m283-3h24-438v (Error.cause sanitization), 92933 bypasses GHSA-v27g-jcqj-v8rw (host-frame stack redaction), 92953 bypasses GHSA-vwrp-x96c-mhwq (host-prototype-pollution fix), 92955 rides the same never-patched __proto__ getter from the vwrp/v6mx/grj5/47x8 lineage, and 92946 is a config-semantics miss under the same resolver that guards other escapes. The dump makes the general rule explicit:
- Pull every historical advisory for a library and list each fix's mechanism, not just its CVE.
- For each mechanism, enumerate the sibling sinks it does not cover. Concrete examples from this wave: the error sanitizer seals plain
Errorhosts in place on first visit but snapshot-rebuildsAggregateError, so a self-cycle or the same aggregate twice inerrors[]gets the raw host proxy handed back by the cycle-break at revisit (92934); the host-frame redaction is applied toError.stackformatting bututil.getCallSites()is a newer API that bypasses it entirely (92933); the protected-intrinsics defense is built from a fixed inventory list of classic globals (Number, String, Boolean, Date, RegExp, Map, WeakMap, Set, WeakSet, Promise, Function) that omits%TypedArray%.prototypeandArrayBuffer.prototype, so__lookupGetter__-based prototype walking still mutates host typed arrays whose effects the host observes afterVM.run()returns (92953). - Operator heuristic: any defense implemented as an enumerated inventory is stale by construction — test the members added to the language/runtime after the inventory was written.
3. Shared memory pools cross the realm boundary silently¶
CVE-2026-92947 and CVE-2026-100723 share one root: Node's small-allocation Buffer pool. Buffer is exposed to the sandbox by default; Buffer.from(...) results are views into a shared pool, and Buffer.from(result.buffer, 0, result.buffer.byteLength) hands the sandbox the whole pool — reading host Buffer.from/Buffer.concat contents (credentials, tokens, plaintext passwords in transit) and flipping bytes in unrelated host buffers. The 100723 variant shows the same invariant missed on the return path of an allowlisted builtin (zlib.deflateSync returns a pooled buffer; vm2's depoolBuffer normalization is applied to sandbox-facing factories but not to host-builtin returns).
Operator value beyond vm2: every realm/isolation boundary that passes view-types over a shared allocator needs a depool check — Node worker threads with transferList/shared ArrayBuffers, quickjs/deno-style bridges, WASI preview buffers. Proof harness is two-sided: allocate a marker in the "host" path, then from the "guest" slice the full backing store at byteOffset 0, full byteLength, and show a byte outside the logical slice is visible/mutuable. The invariant to assert in any report: a buffer crossing into lower trust must own its complete backing store (byteOffset === 0 && buffer.byteLength === length).
4. Engine-scheduled callbacks defeat call-scoped limits¶
Two legs break the "we bound the execution" assumptions that sandbox options promise:
timeoutcovers only the synchronousVM#run()call (CVE-2026-92942):FinalizationRegistry/WeakRefare exposed unhardened; a cleanup callback registered during the run fires later on V8's GC path — never throughdoWithTimeout— so sandbox code runs unbounded after the host believes execution ended, and can block the host event loop from GC context.allowAsync:falseblocksPromise.prototype.thenbut not the statics (CVE-2026-92959):Promise.resolve/all/race/any/allSettledstill assimilate attacker-controlled thenables, invoking theirthenin a microtask afterrun()returned.
Durable rule for validating any code-execution sandbox with time/async limits: after the sanctioned call returns, keep listening. Trigger GC (--expose-gc or memory pressure in the lab), queue microtask/timer/microjob paths through every static helper that accepts an object, and check whether the policy enforcement is on the instance method while static methods reach the same primitive. Enforcement patched at one call site with unpatched siblings is this whole dump's shape — the same wrong-predicate family as the masker/shape-gap legs on the Airflow page.
5. Host error channels are free recon¶
CVE-2026-92936 works with zero configuration: try { eval("@@@ catch") } catch (e) { e.stack } returns a host-realm SyntaxError stack whose formatting bypassed sandbox-side redaction, exposing absolute paths of vm2 internals, Node internals, and the embedding app's source tree. Cheap fingerprint for any JS-sandbox product; also use it to relocate the embedding app on disk before path-based attacks. 92933 is the same redaction gap via util.getCallSites().
Operator triage¶
- Find vm2-as-boundary products: any app, workflow engine, plugin system, low-code platform, or SaaS scripting feature whose docs mention vm2/
NodeVM. vm2 is unmaintained yet still widely shipped; the May 29 triage checklist still applies. - Fingerprint configuration, then version, then Node version: which legs apply depends on
VMvsNodeVM,require.external/root/context,consolemode, builtin allowlist (zlib),timeout/allowAsync, and host Node version (≥26 re-arms the WASM leg). - Prove with the lowest-and-default config first (92956, 92955, 92936 need no opt-ins); escalate to config-gated legs only if the target actually enables them.
- Treat the dump as an audit template for other realm bridges (
isolated-vmalternatives,quickjs-emscripten, plugin hosts): inventory-based protections, view-type crossings, error-channel crossings, engine-scheduled callbacks, and fix genealogy.
Reporting heuristics¶
- Name the exact edge: default VM on Node 26 to host process via WASM streaming Promise species, shared Buffer pool to host credential bytes, README quick-start config to nested-VM host RCE, GC callback to post-timeout host execution, static Promise helper to policy-skipped microtask, sanitizer cycle-revisit to raw host proxy,
util.getCallSitesto unredacted host frames. - Include vm2 commit/version pin, host Node version, exact sandbox options object, the benign marker observed host-side, and — because this fork is abandoned — whether the product can patch at all or must replace the boundary. An unpatchable-boundary finding is materially more severe than a same-CVSS one with a fix release.
- State which legs are configuration-dependent versus default-reachable; do not claim blanket RCE for products whose config excludes the precondition.
October 6 follow-up: the dump keeps landing — two more legs published after this page was drafted¶
Two more advisories from the same coordinated dump (sequential with CVE-2026-100723) published 23:19–23:24Z, after the initial page was committed. Both fit the page's thesis exactly:
NodeVMcustom resolver external-allowlist uses string-prefix regex with no boundary (GHSA-5h3f-q97h-ccvc / CVE-2026-100721, CVSS 10.0):LegacyResolver.customResolvestores an allowlisted module's resolved path as^<path>— no end anchor, no separator. Once the guest requires the allowlisted bare namefoo(which populates the externals cache as^/…/foo), an absolute require of any prefix-sharing sibling (/…/foo2/index.js) passesisPathAllowedForModuleand loads throughhostRequireincontext:'host'— arbitrary host module execution. This is the resolver-side instance of the prefix-match allowlist class (same shape as the Traefik static-file string-prefix escape and CoCart-style prefix checks): a path allowlist implemented asnew RegExp('^' + path)authorizes the whole string-prefix neighborhood, not the directory. Sweep every sandbox/module/URL allowlist for missing$/separator anchoring; the proof is one allowlisted require followed by one sibling require, denial-vs-execution differential.- Sandbox-to-host
constructtrap skips themarkHostPromiseHandledsanitizer the adjacentapplytrap applies (GHSA-2v2p-6j97-cjg9 / CVE-2026-100722, 8.6): a constructable host function returning a rejected native Promise crosses the bridge unhandled viaReflect.construct→ host process dies on Node's strict unhandled-rejection policy. One-line guest PoC; the control that attaches.catch()survives. This is the trap-family parity axis: realm bridges implementapply/construct/get/setproxies and hardening lands on the call trap while the construct trap re-exposes the same primitive. When auditing any JS realm bridge, run every host-Promise-returning shape through both call andnewsyntax. - Adjacent product, same thenable-assimilation class as CVE-2026-92959: Seroval
fromJSON()(GHSA-p6vx-979v-rg4c / CVE-2026-104846, 9.8) — a deserialized fulfilled-Promise node triggers native ECMAScript thenable assimilation invoking plugin-produced callables, bypassing theseroval@1.5.3type-confusion fix (GHSA-mv8w-475r-vwqw). The reusable rule for deserializers that reconstruct live objects (SSR state serializers, RPC codecs): native assimilation semantics re-enter user callables even when the deserializer itself never invokes them — patching the type check misses the language-runtime invocation path. For bounty hunters on SolidStart-adjacent stacks: deserialization desyncs are reachable from SSR-payload handling.
October 7 follow-up: the hardened-fork denylist itself omits child_process (CVE-2026-93605 / GHSA-pq68-rvw4-xp4r, critical, vm2 ≤3.12.0, fixed 3.12.1)¶
A maintained hardening fork of vm2 shipped a DANGEROUS_BUILTINS denylist that blocks host-reachable core modules even under builtin:['*']: module, worker_threads, cluster, vm, repl, inspector, process, trace_events, wasi, diagnostics_channel, async_hooks, perf_hooks, v8, os, dns, test — and omits child_process, the single most direct command-execution primitive. The omission is internally inconsistent (the list denies cluster with the explicit rationale that cluster.fork() spawns a host child process). Guest under require:{builtin:['*']} (or the fork's own documented ['*','-http','-net',…] subtract pattern): require('child_process').execSync(...) → host RCE, one line.
- Durable rule that generalizes past vm2: enumerate a hardening denylist's implied threat model against its membership, item by item. The list author's own rationale comments ("spawns a host child process") name the threat class; every primitive in that class not present in the list is a candidate finding. Mechanical battery for any JS/Python sandbox builtin denylist: process spawning (
child_process,subprocess), env/secrets (process.env,os.environ), dynamic loading paths already in the family, and platform escape hatches (wasiis listed here — proof someone enumerated it). Also note the subtract-pattern trap: documented usage examples like['*','-http','-net']advertise the wildcard posture, so the denylist is the only thing standing — same "the allowlist IS the boundary" thesis as the prefix-anchor leg above. Fingerprint the fork vs upstream by probingrequire('child_process')acceptance under a wildcard builtin config in a lab harness; fix boundary is the fork's 3.12.1.
Tracked without publication (same wave)¶
- Tinypool prototype-pollution gadget → RCE pair (5gmw/85c8, CVE-2026-104848/104849): same worker-options PP-to-exec gadget class as the Oct 1 Piscina follow-up; class canonical on the June 18 page.
- GraphQL Tools pair: legacy WebSocket executor TLS validation disabled (6fw5/CVE-2026-103921) = default-insecure-TLS family on the Oct 2 cert page;
mergeDeepprototype pollution (7mx3/CVE-2026-104852) = canonical PP. - hickory-resolver pack:
lookup()obscuring DNSSEC validation failures (5j98), irrelevant-CNAME following (6f2x), unbounded TC-retry DoS (6w6g) — Rust resolver internals, no new cross-tool axis; revisit if an adjacent product surfaces the DNSSEC-silent-fallback shape. - Quart stray debug print leaking raw request bodies incl. plaintext passwords to stdout (v853) and Snowflake driver log leakage (qqj6/CVE-2026-86597) and
@opentelemetry/instrumentation-*unconditionaldb.userspan attribute (qqmp/CVE-2026-104872): secrets-to-logs class, canonical. - @vue/server-renderer missing-CR attribute-name blacklist XSS (g2v6), ProseMirror paste XSS (c8x8), stream-json JSONC quadratic rescan + assembler PP (hqr4/mjw6), fast-copy stack exhaustion (jggr), PostCSS selector quadratic (rj75/CVE-2026-104844), @fastify/busboy multipart filename/name CRLF injection (gxm5/CVE-2026-74866), Angular SSR Windows sibling-directory traversal in CommonEngine (7g7c/CVE-2026-104871): known classes; the Angular SSR and busboy legs are the two most interesting if follow-up detail lands.
- python-jose DER-encoded-public-key algorithm-confusion guard bypass (3qf3/CVE-2026-85394, Sept-published enrichment landing) and Nodemailer OAuth2 token-fetch TLS validation (r7g4/CVE-2026-82662, June-published re-surfacing): JWT-parser and default-insecure-TLS canonical classes.
- Docling arbitrary local file read via
draw:image xlink:hrefin the OpenDocument backend (4xhp/CVE-2026-105751): document-parser file-read class (September 20 media-pipeline family) and RAG-ingestion relevant — fold on the media page if a second ODT/entity leg lands. - Duplicate-advisory markers GHSA-43rv-jrfh-9mrr, GHSA-5cwm-p7pj-gqp2, GHSA-frj6-5rhh-vwfw; Apache Spark History Server stored XSS (9437/CVE-2026-32773, retro enrichment).