GraphQL Tools: Per-request connection parameters persist into later users' WebSocket authentications
CVE-2026-118665 · GHSA-434m-5hcj-pxf3 · EUVD-2026-112472 · published 28 Oct 2026
How serious is it
- Severity: high, CVSS score 8.2 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-118665?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
@graphql-tools/executor-legacy-ws(npm) <= 1.1.35
How to check
- Run
npm ls @graphql-tools/executor-legacy-wsin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
@graphql-tools/executor-legacy-wsto 1.1.36 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
# Stale authentication state in `@graphql-tools/executor-legacy-ws`: per-request `extensions.connectionParams` persist in executor closure state and are replayed in a later caller's `connection_init`
## 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` (`packages/executors/legacy-ws/src/index.ts`) |
| Vulnerability class | Stale authentication state / credential reuse across requests |
| Verified against | Commit `b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1`; npm release `1.1.33` installed and exercised. During validation the same behaviour was also observed on the current latest release `1.1.35`, and the unreset closure is still present on `master`. |
| API surface | Public documented API (`buildWSLegacyExecutor`, and `request.extensions.connectionParams` on the returned executor) |
| Affected in default configuration | Yes — the factory returns a general, reusable `Executor` with no per-identity or single-use restriction, and no supported teardown clears the retained parameters. |
Version note: the originally recorded affected range was `<= 1.1.33`. Validation showed that range understates exposure — the same proof of concept leaks identically on `1.1.35`.
## Summary
`buildWSLegacyExecutor` keeps a single closure-scoped object, `executorConnectionParams`, for the lifetime of the executor. Every execution that carries `request.extensions.connectionParams` merges those keys into that object with `Object.assign`, and the object is merged into the `connection_init` payload each time a new WebSocket opens. Nothing ever removes keys: `cleanupWebsocket` terminates the socket and nulls the handle but leaves `executorConnectionParams` intact, and a later request that simply omits a key skips the conditional update entirely.
The consumer-side shape is a shared executor that forwards per-user credentials. When one long-lived executor serves multiple principals — the schema-stitching, gateway, or mesh pattern where each `ExecutionRequest` supplies its own `extensions.connectionParams` — a request that omits the auth key is sent to the downstream server with the previous caller's `Authorization`/tenant values in its `connection_init`, so the downstream server authenticates the later caller as the earlier user.
Behaviour of the neighbouring state in the same file:
- WebSocket handle (`websocket`) → reset on teardown in `cleanupWebsocket`, [lines 78-88](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L78-L88)
- Per-request `connectionParams` merge, present case → applied as documented, [lines 93-101](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L93-L101)
- `executorConnectionParams` closure object → **never reset; survives socket teardown and is re-sent on the next connection, [line 32](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L32) and [line 54](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L54)**
## Impact
A caller who supplies no credentials (or lower-privileged ones) can have its downstream GraphQL subscription executed under a previous user's retained token or tenant value, and can receive data that its own `connection_init` would not authorize. The credential itself is never disclosed to the attacker — it reaches the sink through closure storage, not because the attacker knows it — but the downstream authorization it represents is.
The new capability versus safe behaviour: without this bug, an anonymous request would open a connection whose `connection_init` contains only base options plus that request's own parameters, and the downstream server would treat it as anonymous. With it, the anonymous request inherits the victim's authorization. Validation also showed partial-overwrite merging: a later caller supplying only `{tenant: "<attacker tenant>"}` gets the victim's `Authorization` retained alongside its own tenant, i.e. an attacker-chosen tenant authenticated with someone else's bearer token.
Scoring note: **not scored.** No CVSS vector accompanies this finding, and exploitability depends on the consuming application's data flow rather than on the library alone. Consumers that construct a fresh executor per request are **not** affected — the in-repository `UrlLoader` wrapper ([`packages/loaders/url/src/index.ts:172-182`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/loaders/url/src/index.ts#L172-L182)) builds a new executor for each request and therefore does not exhibit the persistence. The exposure is to applications that use the exported factory directly and share the executor.
## Preconditions
1. One executor instance created by `buildWSLegacyExecutor` is reused across requests belonging to different users or authorization contexts.
2. The application uses the explicitly supported `extensions.connectionParams` field to carry per-request downstream identity. This field is documented only by the source comment at [line 91](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L91), which says additional connection params can be supplied through extensions but does not warn that values persist into later requests.
3. The first operation's socket is closed before the later operation opens a new one. This is the normal path, not an edge case: the `COMPLETE` handler calls `cleanupWebsocket()` after every operation.
4. Attacker capability required: the ability to make a request through the sharing application — in a gateway, a remote user …
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/ardatan/graphql-tools/security/advisories/GHSA-434m-5hcj-pxf3
- github.com/ardatan/graphql-tools/pull/8480
- github.com/ardatan/graphql-tools/commit/c2f226e6f68236be9e4a577f29faeaa92547458b
- github.com/ardatan/graphql-tools/releases/tag/@graphql-tools/executor-legacy-ws@1.1.36
- github.com/advisories/GHSA-434m-5hcj-pxf3
- github.com/ardatan/graphql-tools
Merged from GitHub Advisory Database, OSV, ENISA EUVD. Last updated 29 Oct 2026. All vulnerabilities · RSS · JSON