Live › vulnerability › CVE-2026-118665
Go to dashboardThe @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 written from the sources below. Check the originals before acting.
First seen by InfraStack 2026-10-28 21:07 UTC, 0h before NVD. All times on this page are UTC.
Applications that call the exported factory directly and share one executor across users, such as gateway, stitching or mesh setups that forward per-user over legacy graphql-ws subscriptions. The in-repository URL loader builds a new executor per request and is not affected. Single-user applications or those that create a fresh executor per request can skip it.
buildWSLegacyExecutorextensions.connectionParams| Product | Ecosystem | Affected | Fixed in |
|---|---|---|---|
| @graphql-tools/executor-legacy-ws | npm | <= 1.1.35 | 1.1.36 |
npm ls @graphql-tools/executor-legacy-ws to find the installed version in each project.buildWSLegacyExecutor and check whether one executor instance is reused across different users or authorization contexts.extensions.connectionParams to that executor.@graphql-tools/executor-legacy-ws to 1.1.36 or later.Each row is the first time that source carried this signal. The analyst’s rows are its own findings.
@graphql-tools/executor-legacy-ws: per-request extensions.connectionParams persist in executor closure state and are replayed in a later caller's connection_init| 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.
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) → reset on teardown in cleanupWebsocket, lines 78-88connectionParams merge, present case → applied as documented, lines 93-101executorConnectionParams closure object → never reset; survives socket teardown and is re-sent on the next connection, line 32 and line 54A 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) 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.
buildWSLegacyExecutor is reused across requests belonging to different users or authorization contexts.extensions.connectionParams field to carry per-request downstream identity. This field is documented only by the source comment at line 91, which says additional connection params can be supplied through extensions but does not warn that values persist into later requests.COMPLETE handler calls cleanupWebsocket() after every operation.connectionParams key absent.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.
ExecutionRequest supplies extensions.connectionParams, commonly an Authorization or tenant token (packages/executors/legacy-ws/src/index.ts:90-100).Object.assign stores those keys in the closure-scoped executorConnectionParams (index.ts:32, index.ts:97-100).cleanupWebsocket terminates the socket but does not reset executorConnectionParams (index.ts:78-88); the attacker's later request omits connectionParams, so the conditional update is skipped and the old keys remain ([](This page as .md · .json · in your agent: get_signal("CVE-2026-118665")
GitHub Advisory Database, CC-BY 4.0 · NVD data is provided by NIST and is public domain. Posts quoted belong to their authors.
index.ts:93-101