Skip to main content
VulnerabilityPublished

CVE-2026-118666: GraphQL Tools · Shared legacy executor routes one subscription's results to a different caller

Affects @graphql-tools/executor-legacy-wsSource GCVE Database

0
Authoritative record:CVE-2026-118666· Medium 6.0

Not known to be exploited · CVSS 6.0 Medium : upgrade to 1.1.36 when convenient.

Data as of 28 Oct 2026, 22:12 UTC

CVSS 4.0 base score
6.0Medium
How severe the flaw is on the 0–10 CVSS scale.
Patch
Available – fixed in 1.1.36
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

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 28 Oct 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponised: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.

What to do

Upgrade @graphql-tools/executor-legacy-ws to 1.1.36.

Affected software

ProductAffectedFixed in
npm@graphql-tools/executor-legacy-wsAffected
  • <1.1.36
Fixed in
  • 1.1.36

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

# Cross-tenant subscription data leak in `@graphql-tools/executor-legacy-ws`: a shared `websocket.onmessage` is replaced on every `subscribe()`, and `DATA` frames are delivered without checking the protocol operation id ## 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`) | Affected version range: the range recorded for this finding is `<= 1.1.33`, but that is understated — the same proof of concept reproduces on `1.1.35`, and a byte diff of the built CJS output between `1.1.33` and `1.1.35` shows only one unrelated change (`rejectUnauthorized`). ## Summary `buildWSLegacyExecutor` keeps one WebSocket in closure state and installs the message handler by assigning to the socket's single `websocket.onmessage` property inside `subscribe()`. Every new subscription therefore overwrites the previous subscriber's handler, and the handler's `case LEGACY_WS.DATA` branch calls `observer.next(data.payload)` without ever comparing `data.id` to the operation id that this subscription created at line 103 and sent with `START` at line 123. When one executor instance is reused for overlapping subscriptions, the most recent caller receives payloads belonging to the earlier caller, and the earlier caller receives nothing. In a consuming application, the tainted input is an ordinary, correctly-tagged `DATA` frame emitted by the legitimate downstream server for the victim's operation. The attacker does not need to control or compromise that server; they only need to be able to start their own subscription through the same shared executor while the victim's subscription is active — the natural shape of a gateway, stitched schema, or any service that builds a remote executor once and reuses it per request. The surrounding code shows the inconsistency: - `packages/loaders/url/src/index.ts:262-295` (in-repo `UrlLoader` wrapper) → constructs a fresh legacy executor inside each request, so that caller avoids the shared state. - `buildWSLegacyExecutor`'s returned executor, reused across callers → **unsafe: shared `onmessage` overwritten at line 113, `DATA` dispatched without an id check at lines 145-147**. - `packages/loaders/url/src/index.ts:172-182` → also a production caller of the legacy executor; safe only insofar as the wrapper above rebuilds the executor per request, not because of any check in the executor itself. ## Impact A user of a service that shares one legacy executor receives another user's private GraphQL subscription payloads verbatim, with the victim's authorization context, without ever having been authorized for that operation. The victim is simultaneously starved: their own iterator stops receiving the events they subscribed to, because their handler has been detached. Without this bug, the attacker's observer would receive only results for their own operation — and in this implementation, no result at all, since their operation is never `START`ed after `CONNECTION_ACK` has already fired. The new capability is therefore reading data the attacker could not otherwise obtain by any request they are permitted to make, plus a denial of the victim's subscription stream. Scoring note: no CVSS vector was computed for this finding, because exploitability depends on the consuming application's data flow — specifically whether one executor instance is shared across mutually untrusted callers. Consumers that construct a new executor per operation (as the in-repo `UrlLoader` wrapper does at `packages/loaders/url/src/index.ts:262-295`) are not affected by the cross-caller leak. The set of affected downstream consumers has not been enumerated: **unknown**. ## Preconditions 1. One executor value returned by `buildWSLegacyExecutor` is reused rather than constructing a new executor per operation. This is the normal use of an `Executor`: the contract is invoked once per `ExecutionRequest`, the code explicitly reuses an `OPEN` socket, and the returned value holds `Symbol.disp

References

Community reviews

Loading reviews…