---
id: "CVE-2026-118665"
kind: "vulnerability"
aliases: ["CVE-2026-118665", "GHSA-434m-5hcj-pxf3"]
title: "GraphQL Tools: Per-request connection parameters persist into later users' WebSocket authentications"
severity: "high"
cvss: 8.2
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "@graphql-tools/executor-legacy-ws", "fixed": ["1.1.36"], "affected": "<= 1.1.35", "ecosystem": "npm"}]
ecosystems: ["npm"]
tags: ["cwe-613", "gateway", "graphql", "graphql-tools", "node.js", "npm", "package", "schema-stitching", "websocket"]
not_affected_if: ["fresh-executor-per-request", "no-connection-params", "single-user"]
published_at: "2026-10-28T21:05:17+00:00"
first_seen_at: "2026-10-28T21:07:44+00:00"
updated_at: "2026-10-28T21:35:45+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-434m-5hcj-pxf3", "first_seen_at": "2026-10-28T21:07:44+00:00"}, {"source": "nvd", "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-118665", "first_seen_at": "2026-10-28T21:35:45+00:00"}]
---

# GraphQL Tools: Per-request connection parameters persist into later users' WebSocket authentications

The @graphql-tools/executor-legacy-ws package keeps connection parameters from one request in executor state and sends them in the connection_init payload of later requests on the same executor. A later caller that omits its own credentials can therefore have its downstream subscription authorized as the earlier user.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- npm:@graphql-tools/executor-legacy-ws affected <= 1.1.35 fixed 1.1.36

## How to check

- Run `npm ls @graphql-tools/executor-legacy-ws` to find the installed version in each project.
- Search application code for `buildWSLegacyExecutor` and check whether one executor instance is reused across different users or authorization contexts.
- Check whether requests pass `extensions.connectionParams` to that executor.

## Fix

- Upgrade `@graphql-tools/executor-legacy-ws` to `1.1.36` or later.
- Until upgraded, create a fresh executor per request or per authorization context instead of sharing one.

## Timeline

- 2026-10-28T21:07:44+00:00 ghsa https://github.com/advisories/GHSA-434m-5hcj-pxf3
- 2026-10-28T21:35:45+00:00 nvd https://nvd.nist.gov/vuln/detail/CVE-2026-118665

## From the sources

# 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 controls whether they authenticate and which subscription they ask for, and an anonymous request naturally leaves the `connectionParams` key absent.
5. The later request omits at least one sensitive key rather than overwriting every prior key (`Object.assign` merges, it does not replace).

This does **not** require non-standard usage, monkey-patching, or unusual configuration: the affected path is the documented `extensions.connectionParams` feature on the package's public API, and the library does not document per-request credential isolation as a caller responsibility. Validation confirmed that the library's own documented teardown, `executor[Symbol.dispose]()` (bound to `cleanupWebsocket`), does not clear the state either — no supported API resets it.

## Data flow

1. **Source** — a victim `ExecutionRequest` supplies `extensions.connectionParams`, commonly an `Authorization` or tenant token ([`packages/executors/legacy-ws/src/index.ts:90-100`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L90-L100)).
2. **Propagation** — `Object.assign` stores those keys in the closure-scoped `executorConnectionParams` ([`index.ts:32`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L32), [`index.ts:97-100`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L97-L100)).
3. **Propagation** — `cleanupWebsocket` terminates the socket but does not reset `executorConnectionParams` ([`index.ts:78-88`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L78-L88)); the attacker's later request omits `connectionParams`, so the conditional update is skipped and the old keys remain ([`index.ts:93-101`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcb

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
