GHSA-434M-5HCJ-PXF3
Vulnerability from github – Published: 2026-10-28 21:05 – Updated: 2026-10-28 21:05Stale 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 incleanupWebsocket, lines 78-88 - Per-request
connectionParamsmerge, present case → applied as documented, lines 93-101 executorConnectionParamsclosure object → never reset; survives socket teardown and is re-sent on the next connection, line 32 and line 54
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) 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
- One executor instance created by
buildWSLegacyExecutoris reused across requests belonging to different users or authorization contexts. - The application uses the explicitly supported
extensions.connectionParamsfield 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. - 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
COMPLETEhandler callscleanupWebsocket()after every operation. - 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
connectionParamskey absent. - The later request omits at least one sensitive key rather than overwriting every prior key (
Object.assignmerges, 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
- Source — a victim
ExecutionRequestsuppliesextensions.connectionParams, commonly anAuthorizationor tenant token (packages/executors/legacy-ws/src/index.ts:90-100). - Propagation —
Object.assignstores those keys in the closure-scopedexecutorConnectionParams(index.ts:32,index.ts:97-100). - Propagation —
cleanupWebsocketterminates the socket but does not resetexecutorConnectionParams(index.ts:78-88); the attacker's later request omitsconnectionParams, so the conditional update is skipped and the old keys remain (index.ts:93-101). - Sink — the new socket's
onopenmerges the retained state into theconnection_initpayload and sends it before the attacker's operation:payload = Object.assign(payload, executorConnectionParams); websocket!.send(...connection_init payload...)(index.ts:44-65,index.ts:120-129). - Reachability — the package directly exports
buildWSLegacyExecutor(index.ts:27), and the entry point is the returned disposable executor plusrequest.extensions.connectionParams.
Vulnerable code
packages/executors/legacy-ws/src/index.ts, lines 32-100 at b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1:
let executorConnectionParams = {}; // closure state, lives as long as the executor
...
payload = Object.assign(payload, executorConnectionParams); // sink: retained keys enter connection_init
websocket!.send(JSON.stringify({ type: LEGACY_WS.CONNECTION_INIT, payload }));
...
if (request.extensions?.['connectionParams'] && typeof request.extensions?.['connectionParams'] === 'object') {
executorConnectionParams = Object.assign(executorConnectionParams, request.extensions['connectionParams']);
}
// no else branch: a request that omits connectionParams leaves the previous request's keys in place,
// and cleanupWebsocket (lines 78-88) resets only `websocket`, never `executorConnectionParams`.
Reproduction
Verified dynamically against the real package, not by static review alone. Release 1.1.33 was installed from npm and driven against a live local ws server implementing the legacy graphql-ws protocol, recording the connection_init payload of every physical connection.
Observed across the validation runs, using one shared executor instance:
- Request 1 (victim) sent
extensions.connectionParams{Authorization: "Bearer VICTIM-ALICE-SECRET-TOKEN", tenant: "alice-tenant"}. After that subscription completed, the executor sentconnection_terminateand tore the socket down. - Request 2 supplied no
connectionParams, yet the server observed a brand-new connection whoseconnection_initpayload was byte-identical to the victim's, including the victim's bearer token. - Partial overwrite: when the later caller supplied only
{tenant: "mallory-tenant"},Object.assignmerged rather than replaced, so the victim'sAuthorizationwas retained alongside the attacker's tenant. - Calling
executor[Symbol.dispose]()between requests did not clear the state. - Negative control: patching the installed
1.1.33to addexecutorConnectionParams = {}insidecleanupWebsocketmade the second connection's init payload{}, so the leak is causally attributable to the missing reset. - The same proof of concept leaks identically on
1.1.35, andmasterstill contains the unreset closure. Upstream line numbers onmasterare slightly offset from those cited above (closure declaration L38, conditionalObject.assignL100-105,cleanupWebsocketL84,onopenmerge L60-63).
Steps:
- Run a legacy GraphQL WS server that records the
connection_initpayload and authorizes notifications byAuthorizationvalue. - Use one executor for a victim request with
extensions.connectionParams.Authorization = 'Bearer victim-token'; finish/unsubscribe it so cleanup closes the socket. - Use the same executor for an anonymous request with no
extensions.connectionParams. - Observe that the second
connection_initstill containsBearer victim-tokenand that the anonymous subscription receives victim-authorized results.
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 v = exec({
document: parse('subscription { privateEvents { id } }'),
extensions: { connectionParams: { Authorization: 'Bearer victim-token' } },
});
await v.next();
await v.return?.(); // iterator cleanup closes the first socket; executorConnectionParams is not cleared
const a = exec({ document: parse('subscription { privateEvents { id } }') });
await a.next(); // the server logs a second connection_init payload still carrying Authorization: Bearer victim-token
- Expected: the second
connection_initpayload contains only the base options and the second request's own parameters, with no key omitted by that request carried over from the victim. - Actual: the second
connection_initcarries the victim's retainedAuthorization/tenant values, and the downstream server authenticates the later caller as the victim.
Suggested remediation
Do not retain operation-level extensions.connectionParams beyond the operation and connection they belong to: build each connection_init payload from the base options plus the current request's parameters only, and clear connection-specific state during cleanup.
// 1. Clear the retained state when the connection is torn down.
function cleanupWebsocket() {
// ... existing connection_terminate / terminate / websocket = null ...
executorConnectionParams = {};
}
Alternative, and the more complete form: stop accumulating into shared closure state at all — derive the payload per request, e.g. const payload = { ...options.connectionParams, ...(request.extensions?.['connectionParams'] ?? {}) }, passed down to the onopen handler for that connection rather than read from the executor closure.
Notes:
executor[Symbol.dispose]()is bound tocleanupWebsocket, so fixing the reset there also makes the documented teardown behave as callers expect.Object.assignmerges rather than replaces, so a later request supplying some parameters still inherits the keys it omits; a fix that only replaces the object on the "present" branch would leave this half-open.- The
UrlLoaderpath in this repository constructs a fresh executor per request and is already safe; no change is needed there. The exposure is to direct consumers of the exportedbuildWSLegacyExecutor. - It would also help to define explicitly whether mixing distinct auth contexts on one executor is supported, and to reject it if not — today the factory returns a general
Executorwith no stated per-identity restriction.
Suggested regression test alongside the package's existing legacy-ws executor tests (the exact spec file is not verified by the available information): drive one executor against a recording server, send request 1 with extensions.connectionParams = { Authorization: 'Bearer victim-token' }, let it complete, then send request 2 with no connectionParams, and assert the second connection_init payload contains no Authorization key. Add a second case where request 2 supplies only { tenant: 'other' } and assert the payload is exactly { tenant: 'other' } plus base options.
Disclosure status
Currently undisclosed: no CVE, GHSA, advisory URL, upstream issue, or fix commit is associated with this finding, and no fixed version exists to upgrade to.
Resolution
Fixed in @graphql-tools/executor-legacy-ws@1.1.36, which is published on npm. Each execution opens its own socket. connection_init is built from the executor's base connectionParams plus only that request's extensions.connectionParams. Upgrade to >=1.1.36.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 1.1.35"
},
"package": {
"ecosystem": "npm",
"name": "@graphql-tools/executor-legacy-ws"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "1.1.36"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-118665"
],
"database_specific": {
"cwe_ids": [
"CWE-613"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-28T21:05:17Z",
"nvd_published_at": null,
"severity": "HIGH"
},
"details": "# 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\u0027s `connection_init`\n\n## Report metadata\n\n| Field | Value |\n| --- | --- |\n| Package | `@graphql-tools/executor-legacy-ws` (npm, `pkg:npm/%40graphql-tools/executor-legacy-ws`) |\n| Repository | https://github.com/ardatan/graphql-tools |\n| Component | `buildWSLegacyExecutor` (`packages/executors/legacy-ws/src/index.ts`) |\n| Vulnerability class | Stale authentication state / credential reuse across requests |\n| 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`. |\n| API surface | Public documented API (`buildWSLegacyExecutor`, and `request.extensions.connectionParams` on the returned executor) |\n| Affected in default configuration | Yes \u2014 the factory returns a general, reusable `Executor` with no per-identity or single-use restriction, and no supported teardown clears the retained parameters. |\n\nVersion note: the originally recorded affected range was `\u003c= 1.1.33`. Validation showed that range understates exposure \u2014 the same proof of concept leaks identically on `1.1.35`.\n\n## Summary\n\n`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.\n\nThe consumer-side shape is a shared executor that forwards per-user credentials. When one long-lived executor serves multiple principals \u2014 the schema-stitching, gateway, or mesh pattern where each `ExecutionRequest` supplies its own `extensions.connectionParams` \u2014 a request that omits the auth key is sent to the downstream server with the previous caller\u0027s `Authorization`/tenant values in its `connection_init`, so the downstream server authenticates the later caller as the earlier user.\n\nBehaviour of the neighbouring state in the same file:\n\n- WebSocket handle (`websocket`) \u2192 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)\n- Per-request `connectionParams` merge, present case \u2192 applied as documented, [lines 93-101](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L93-L101)\n- `executorConnectionParams` closure object \u2192 **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)**\n\n## Impact\n\nA caller who supplies no credentials (or lower-privileged ones) can have its downstream GraphQL subscription executed under a previous user\u0027s 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 \u2014 it reaches the sink through closure storage, not because the attacker knows it \u2014 but the downstream authorization it represents is.\n\nThe 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\u0027s own parameters, and the downstream server would treat it as anonymous. With it, the anonymous request inherits the victim\u0027s authorization. Validation also showed partial-overwrite merging: a later caller supplying only `{tenant: \"\u003cattacker tenant\u003e\"}` gets the victim\u0027s `Authorization` retained alongside its own tenant, i.e. an attacker-chosen tenant authenticated with someone else\u0027s bearer token.\n\nScoring note: **not scored.** No CVSS vector accompanies this finding, and exploitability depends on the consuming application\u0027s data flow rather than on the library alone. Consumers that construct a fresh executor per request are **not** affected \u2014 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.\n\n## Preconditions\n\n1. One executor instance created by `buildWSLegacyExecutor` is reused across requests belonging to different users or authorization contexts.\n2. 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.\n3. The first operation\u0027s 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.\n4. Attacker capability required: the ability to make a request through the sharing application \u2014 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.\n5. The later request omits at least one sensitive key rather than overwriting every prior key (`Object.assign` merges, it does not replace).\n\nThis does **not** require non-standard usage, monkey-patching, or unusual configuration: the affected path is the documented `extensions.connectionParams` feature on the package\u0027s public API, and the library does not document per-request credential isolation as a caller responsibility. Validation confirmed that the library\u0027s own documented teardown, `executor[Symbol.dispose]()` (bound to `cleanupWebsocket`), does not clear the state either \u2014 no supported API resets it.\n\n## Data flow\n\n1. **Source** \u2014 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)).\n2. **Propagation** \u2014 `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)).\n3. **Propagation** \u2014 `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\u0027s 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/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L93-L101)).\n4. **Sink** \u2014 the new socket\u0027s `onopen` merges the retained state into the `connection_init` payload and sends it before the attacker\u0027s operation: `payload = Object.assign(payload, executorConnectionParams); websocket!.send(...connection_init payload...)` ([`index.ts:44-65`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L44-L65), [`index.ts:120-129`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L120-L129)).\n5. **Reachability** \u2014 the package directly exports `buildWSLegacyExecutor` ([`index.ts:27`](https://github.com/ardatan/graphql-tools/blob/b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1/packages/executors/legacy-ws/src/index.ts#L27)), and the entry point is the returned disposable executor plus `request.extensions.connectionParams`.\n\n## Vulnerable code\n\n`packages/executors/legacy-ws/src/index.ts`, lines 32-100 at `b5d1c36ebb8ed3868260ffee5bbcbb423ad5c1f1`:\n\n```ts\nlet executorConnectionParams = {}; // closure state, lives as long as the executor\n...\npayload = Object.assign(payload, executorConnectionParams); // sink: retained keys enter connection_init\nwebsocket!.send(JSON.stringify({ type: LEGACY_WS.CONNECTION_INIT, payload }));\n...\nif (request.extensions?.[\u0027connectionParams\u0027] \u0026\u0026 typeof request.extensions?.[\u0027connectionParams\u0027] === \u0027object\u0027) {\n executorConnectionParams = Object.assign(executorConnectionParams, request.extensions[\u0027connectionParams\u0027]);\n}\n// no else branch: a request that omits connectionParams leaves the previous request\u0027s keys in place,\n// and cleanupWebsocket (lines 78-88) resets only `websocket`, never `executorConnectionParams`.\n```\n\n## Reproduction\n\n**Verified dynamically against the real package**, not by static review alone. Release `1.1.33` was installed from npm and driven against a live local `ws` server implementing the legacy `graphql-ws` protocol, recording the `connection_init` payload of every physical connection.\n\nObserved across the validation runs, using one shared executor instance:\n\n- Request 1 (victim) sent `extensions.connectionParams` `{Authorization: \"Bearer VICTIM-ALICE-SECRET-TOKEN\", tenant: \"alice-tenant\"}`. After that subscription completed, the executor sent `connection_terminate` and tore the socket down.\n- Request 2 supplied **no** `connectionParams`, yet the server observed a brand-new connection whose `connection_init` payload was byte-identical to the victim\u0027s, including the victim\u0027s bearer token.\n- Partial overwrite: when the later caller supplied only `{tenant: \"mallory-tenant\"}`, `Object.assign` merged rather than replaced, so the victim\u0027s `Authorization` was retained alongside the attacker\u0027s tenant.\n- Calling `executor[Symbol.dispose]()` between requests did not clear the state.\n- Negative control: patching the installed `1.1.33` to add `executorConnectionParams = {}` inside `cleanupWebsocket` made the second connection\u0027s init payload `{}`, so the leak is causally attributable to the missing reset.\n- The same proof of concept leaks identically on `1.1.35`, and `master` still contains the unreset closure. Upstream line numbers on `master` are slightly offset from those cited above (closure declaration L38, conditional `Object.assign` L100-105, `cleanupWebsocket` L84, `onopen` merge L60-63).\n\nSteps:\n\n1. Run a legacy GraphQL WS server that records the `connection_init` payload and authorizes notifications by `Authorization` value.\n2. Use one executor for a victim request with `extensions.connectionParams.Authorization = \u0027Bearer victim-token\u0027`; finish/unsubscribe it so cleanup closes the socket.\n3. Use the same executor for an anonymous request with no `extensions.connectionParams`.\n4. Observe that the second `connection_init` still contains `Bearer victim-token` and that the anonymous subscription receives victim-authorized results.\n\n```ts\nimport WebSocket from \u0027isomorphic-ws\u0027;\nimport { parse } from \u0027graphql\u0027;\nimport { buildWSLegacyExecutor } from \u0027@graphql-tools/executor-legacy-ws\u0027;\n\nconst exec = buildWSLegacyExecutor(\u0027ws://localhost:8081/graphql\u0027, WebSocket);\n\nconst v = exec({\n document: parse(\u0027subscription { privateEvents { id } }\u0027),\n extensions: { connectionParams: { Authorization: \u0027Bearer victim-token\u0027 } },\n});\nawait v.next();\nawait v.return?.(); // iterator cleanup closes the first socket; executorConnectionParams is not cleared\n\nconst a = exec({ document: parse(\u0027subscription { privateEvents { id } }\u0027) });\nawait a.next(); // the server logs a second connection_init payload still carrying Authorization: Bearer victim-token\n```\n\n- **Expected:** the second `connection_init` payload contains only the base options and the second request\u0027s own parameters, with no key omitted by that request carried over from the victim.\n- **Actual:** the second `connection_init` carries the victim\u0027s retained `Authorization`/tenant values, and the downstream server authenticates the later caller as the victim.\n\n## Suggested remediation\n\nDo not retain operation-level `extensions.connectionParams` beyond the operation and connection they belong to: build each `connection_init` payload from the base options plus the current request\u0027s parameters only, and clear connection-specific state during cleanup.\n\n```ts\n// 1. Clear the retained state when the connection is torn down.\nfunction cleanupWebsocket() {\n // ... existing connection_terminate / terminate / websocket = null ...\n executorConnectionParams = {};\n}\n```\n\nAlternative, and the more complete form: stop accumulating into shared closure state at all \u2014 derive the payload per request, e.g. `const payload = { ...options.connectionParams, ...(request.extensions?.[\u0027connectionParams\u0027] ?? {}) }`, passed down to the `onopen` handler for that connection rather than read from the executor closure.\n\nNotes:\n\n- `executor[Symbol.dispose]()` is bound to `cleanupWebsocket`, so fixing the reset there also makes the documented teardown behave as callers expect.\n- `Object.assign` merges rather than replaces, so a later request supplying *some* parameters still inherits the keys it omits; a fix that only replaces the object on the \"present\" branch would leave this half-open.\n- The `UrlLoader` path in this repository constructs a fresh executor per request and is already safe; no change is needed there. The exposure is to direct consumers of the exported `buildWSLegacyExecutor`.\n- It would also help to define explicitly whether mixing distinct auth contexts on one executor is supported, and to reject it if not \u2014 today the factory returns a general `Executor` with no stated per-identity restriction.\n\nSuggested regression test alongside the package\u0027s existing legacy-ws executor tests (the exact spec file is **not verified** by the available information): drive one executor against a recording server, send request 1 with `extensions.connectionParams = { Authorization: \u0027Bearer victim-token\u0027 }`, let it complete, then send request 2 with no `connectionParams`, and assert the second `connection_init` payload contains no `Authorization` key. Add a second case where request 2 supplies only `{ tenant: \u0027other\u0027 }` and assert the payload is exactly `{ tenant: \u0027other\u0027 }` plus base options.\n\n## Disclosure status\n\nCurrently undisclosed: no CVE, GHSA, advisory URL, upstream issue, or fix commit is associated with this finding, and no fixed version exists to upgrade to.\n\n## Resolution\n\nFixed in `@graphql-tools/executor-legacy-ws@1.1.36`, which is published on npm. Each execution opens its own socket. `connection_init` is built from the executor\u0027s base `connectionParams` plus only that request\u0027s `extensions.connectionParams`. Upgrade to `\u003e=1.1.36`.",
"id": "GHSA-434m-5hcj-pxf3",
"modified": "2026-10-28T21:05:17Z",
"published": "2026-10-28T21:05:17Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/ardatan/graphql-tools/security/advisories/GHSA-434m-5hcj-pxf3"
},
{
"type": "WEB",
"url": "https://github.com/ardatan/graphql-tools/pull/8480"
},
{
"type": "WEB",
"url": "https://github.com/ardatan/graphql-tools/commit/c2f226e6f68236be9e4a577f29faeaa92547458b"
},
{
"type": "PACKAGE",
"url": "https://github.com/ardatan/graphql-tools"
},
{
"type": "WEB",
"url": "https://github.com/ardatan/graphql-tools/releases/tag/@graphql-tools/executor-legacy-ws@1.1.36"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:4.0/AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N",
"type": "CVSS_V4"
}
],
"summary": "GraphQL Tools: Per-request connection parameters persist into later users\u0027 WebSocket authentications"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.