KEVWire

Security intelligence, with context.

Response snapshot Oct 29, 2026, 01:26 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-118666

GraphQL Tools: Shared legacy executor routes one subscription's results to a different caller

| Field | Value | | --- | --- | | Package | @graphql-tools/executor-legacy-ws (npm, pkg:npm/%40graphql-tools/executor-legacy-ws) | | Repository | https://github.com/ardatan/graphql-tools | | Component | buildWSLegacyExecutor → the returned executor's subscribe(observer) / websocket.

At a glance

Exploited
Not known
Severity
6.0 Medium (CVSS 4.0)
Chance of exploitation
Not estimated
Sources
1 source

What to do

GitHub lists first patched versions: @graphql-tools/executor-legacy-ws: 1.1.36. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-mp66-8ww2-pcwm (opens a new tab)

What happened

Report metadata

| Field | Value | | --- | --- | | Package | @graphql-tools/executor-legacy-ws (npm, pkg:npm/%40graphql-tools/executor-legacy-ws) | | Repository | https://github.com/ardatan/graphql-tools | | Component | buildWSLegacyExecutor → the returned executor's subscribe(observer) / websocket.

onmessage handler | | Vulnerability class | Cross-tenant data leak (subscription results delivered to the wrong caller) | | Verified against | Source at b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1 (package version 1.1.33); published npm tarball 1.1.33 with matching source at d304214db06e8a4ce9cb41138f199e3bc6ed36f2; the same behaviour also reproduces on the current release 1.1.

35 | | API surface | Public documented API (buildWSLegacyExecutor) | | Affected in default configuration | Yes — ensureWebsocket() reuses an already-OPEN socket (index.ts:35-42), and the socket has a single mutable onmessage property (index.ts:113). No single-operation-only restriction is documented, and the return type is the general reusable DisposableExecutor (packages/utils/src/executor.ts:34-57) |

Continue reading

Affected version range: the range recorded for this finding is ` { // sole handler: overwritten by every new subscribe() const data = JSON.parse(event.data.toString('utf-8')); switch (data.type) { ... case LEGACY_WS.DATA: { observer.next(data.payload); // SINK: data.id is never compared with id break; } } }; }, });


Two independent defects are visible here: the single mutable `onmessage` assignment (which detaches the earlier subscriber), and the missing `data.id === id` check in the `DATA` branch (which misroutes the payload). The operation id itself is also derived from `Date.now()`, so it is not collision-resistant.

## Reproduction

Verified dynamically against the real published package `@graphql-tools/executor-legacy-ws@1.1.33` (npm tarball with matching source at `d304214db06e8a4ce9cb41138f199e3bc6ed36f2`). The harness was a protocol-correct legacy `graphql-ws` server (ws v8) that tags every `DATA` frame with the operation id of the operation that produced it, plus two callers sharing one executor instance.

- **Scenario A (control, single subscription):** the subscriber received its own payload, confirming the harness is sound.
- **Scenario B (the claim):** caller 1 ("victim") subscribed; the server ACKed and the client sent `start id=1790247741390`. Caller 2 ("attacker") then invoked the **same** executor while `readyState` was `OPEN`. The server observed only one socket (`ensureWebsocket` reused it) and only one started operation — the attacker's operation was never started, because `CONNECTION_ACK` had already fired. The server then emitted a correctly-tagged `DATA` frame for the victim's id. The attacker's iterator received `{"data":{"victimSecret":"VICTIM-PRIVATE-DATA-a1b2c3"}}` and the victim's iterator received nothing.
- **Scenario C:** a `DATA` frame tagged with the id `"id-that-was-never-started"` was still delivered to the subscriber, proving the protocol operation id is never validated.
- **Realistic wiring:** with the executor wired into `wrapSchema()` the way a stitching gateway does, two concurrent users subscribing to the same field produced a verbatim cross-tenant leak — User B received `{"data":{"accountBalance":"USER-A-PRIVATE-BALANCE-$91402.11"}}` while User A received nothing. When the two users select different fields, the wrap layer's result transform masks the foreign field (B saw `weather:null`), but the misrouting still occurs and the victim is still starved; at the raw executor API the payload leaks verbatim regardless.
- **Causality control:** patching only the `DATA` branch to check `data.id === id` eliminated the leak (the attacker received nothing, and the foreign-id frame was rejected), pinning the leak to the missing id check. Under that patch the victim still received nothing, confirming the second, independent defect: the single shared `websocket.onmessage` is overwritten on each `subscribe()`, detaching the earlier subscriber's handler entirely.
- **Scope caveat:** the identical proof of concept also reproduces on the current latest release `1.1.35`.

Minimal client-side shape:

ts import WebSocket from 'isomorphic-ws'; import { parse } from 'graphql'; import { buildWSLegacyExecutor } from '@graphql-tools/executor-legacy-ws';

const exec = buildWSLegacyExecutor('ws://localhost:8081/graphql', WebSocket);

const victim = exec({ document: parse('subscription { privateEvent { secret } }') }); const victimPending = victim.next(); // causes connection + START; wait until the server has ACKed await new Promise(r => setTimeout(r, 100));

const attacker = exec({ document: parse('subscription { publicEvent { id } }') }); const attackerPending = attacker.next(); // overwrites socket.onmessage

// When the server now emits DATA for the victim's id, this prints the victim's secret. console.log(await attackerPending);


Steps:

1. Start a normal legacy `subscriptions-transport-ws` server with a private event subscription.
2. Build one executor and call `victimIterator.next()` so the socket receives `connection_ack` and `START` for the victim.
3. While that socket is `OPEN`, call `attackerIterator.next()` using the same executor.
4. Emit one victim event from the server.

- **Expected:** a `DATA` frame is delivered only to the observer registered for `data.id`; the attacker's pending `next()` must never resolve with the victim's payload, and the victim keeps receiving their own events.
- **Actual:** the attacker's `next()` resolves with the victim's payload, and the victim's iterator receives nothing.

## Suggested remediation

Dispatch protocol frames by operation id to the observer that started that operation, instead of 

Affected package ranges reported by GitHub:
@graphql-tools/executor-legacy-ws: <= 1.1.35

Scores and how they were measured

CVSS base severity

6.0

Version 4.0As of

CVSS:4.0/AV:N/AC:H/AT:P/PR:L/UI:N/VC:H/VI:N/VA:L/SC:N/SI:N/SA:N

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

Not available

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-mp66-8ww2-pcwm (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-mp66-8ww2-pcwm, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 28). GraphQL Tools: Shared legacy executor routes one subscription's results to a different caller (CVE-2026-118666). https://kevwire.com/record/CVE-2026-118666

MLA

"GraphQL Tools: Shared legacy executor routes one subscription's results to a different caller (CVE-2026-118666)." KEVWire, 28 Oct. 2026, kevwire.com/record/CVE-2026-118666.

Chicago

KEVWire. "GraphQL Tools: Shared legacy executor routes one subscription's results to a different caller (CVE-2026-118666)." Last modified October 28, 2026. https://kevwire.com/record/CVE-2026-118666.

Evidence summary (plain text)

GraphQL Tools: Shared legacy executor routes one subscription's results to a different caller (Oct 28, 2026).
| Field | Value |
| --- | --- |
| Package | @graphql-tools/executor-legacy-ws (npm, pkg:npm/%40graphql-tools/executor-legacy-ws) |
| Repository | https://github.com/ardatan/graphql-tools |
| Component | buildWSLegacyExecutor → the returned executor's subscribe(observer) / websocket.onmessage handler |
| Vulnerability class | Cross-tenant data leak (subscription results delivered to the wrong caller) |
| Verified against | Source at b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1 (package version 1.1.33); published npm tarball 1.1.33 with matching source at d304214db06e8a4ce9cb41138f199e3bc6ed36f2;…
CVSS 6.0 (version 4.0; evidence as of Oct 28, 2026; https://github.com/advisories/GHSA-mp66-8ww2-pcwm).
Sources:
GitHub-reviewed advisory GHSA-mp66-8ww2-pcwm: https://github.com/advisories/GHSA-mp66-8ww2-pcwm
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc
Record ID: CVE-2026-118666.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 29, 2026, 00:55 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are coming as part of KEVWire Premium: join the waitlist. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail