
Cloud Vulnerability DB
A community-led vulnerabilities database
| Field | Value |
|---|---|
| Package | @graphql-tools/executor-http (npm, pkg:npm/%40graphql-tools/executor-http) |
| Repository | https://github.com/graphql-hive/gateway |
| Component | buildHTTPExecutor / handleInflightRequest (packages/executors/http/src/index.ts) |
| Vulnerability class | Information disclosure / authorization bypass via cache (response reuse across security contexts) |
| CWE | CWE-524 |
| Severity (reporter assessment) | High |
| Verified against | Source read at tag @graphql-tools/executor-http@3.3.0 (commit 0778df53dd9450c2b1d6fcf121832b198fe75d67); line references below pinned to commit a0d7bd84f2c655b67410831ae8119bc26c351cc4; published npm artifacts 2.1.2, 3.0.0, 3.3.0 and 3.4.1 exercised dynamically |
| API surface | Public documented API (buildHTTPExecutor, and the documented fetch option) |
| Affected in default configuration | Yes — deduplicateInflightRequests is documented @default true and is only bypassed when explicitly set to false (packages/executors/http/src/index.ts:150-154) |
| Affected versions | >= 3.0.0 through at least 3.4.1. Inflight deduplication landed in 3.0.0 (per the package CHANGELOG); 2.1.2 has no deduplication code and does not leak; 3.0.0, 3.3.0 and the latest release at time of validation, 3.4.1, all reproduce |
| Fixed version | None known — no fixed release existed at time of validation |
| Public advisory / CVE / GHSA | None (unknown / not assigned) |
buildHTTPExecutor deduplicates concurrent requests using a module-level Map keyed only by URL, HTTP method, the serialized request body, and — only when isCustomHeader is true — the already-materialized library-visible headers. Neither the ExecutionRequest's context, nor info, nor the identity of the fetch implementation, nor the executor instance participates in that key. The documented fetch hook is explicitly handed each request's context and info, so a hook that derives upstream authorization (for example a per-user bearer token) from context runs only for the first of two concurrent identical queries; the second caller receives the first caller's already-authorized response.
In a consuming application, any remote GraphQL caller controls its own document and variables and can therefore choose a query a victim is also making. Because dedup is keyed on the serialized body, the attacker's request selects the victim's inflight Promise before the attacker's own fetch hook can apply the attacker's authorization. The option's own comment promises deduplication of "requests with the same parameters", but two requests with different authorization context are not security-equivalent.
Sibling paths in the same code, which is what makes this look like a bug rather than a documented trade-off:
options.headers / request.extensions.headers -> safe: headers join the dedup key when present (index.ts:568-570); confirmed non-leaking in testing.fetch hook from context/info -> unsafe: context and info never enter the key (index.ts:561-570).deduplicateInflightRequests: false -> safe: each request runs its own fetch with its own context; confirmed non-leaking in testing.An attacker who races a query that an authenticated victim is making receives the victim's upstream GraphQL response — data the upstream service authorized for the victim's identity or tenant, not the attacker's. In the validated reproduction this was a cross-tenant authorization bypass: the second user received the first user's record, including a sensitive per-user field, while the upstream service saw only the first user's bearer token.
The delta versus safe behaviour: without Promise reuse, fetchFn would run with the attacker's own context, the hook would attach the attacker's token, and the upstream service would return only attacker-visible data. The bug converts a correctly context-enforcing fetch wrapper into a no-op for every deduplicated request. Because the inflight Map is module-level rather than per-executor, the reuse was observed to cross two independent buildHTTPExecutor instances with distinct fetch hooks in the same process.
Scoring note: no CVSS vector is asserted here. Exploitability depends on the consuming application's data flow — specifically whether per-request upstream authorization is applied inside the fetch hook (affected) or entirely through library-visible headers (not affected, because those headers enter the dedup key). Consumers that set deduplicateInflightRequests: false, that never pass a custom fetch, or whose upstream responses are identical for all callers are not affected. The reporter assessment of High reflects the unauthenticated-to-authenticated data disclosure in the default configuration for the affected shape.
deduplicateInflightRequests is omitted or true — the documented default is true (index.ts:150-154).fetch hook using its context or info argument, rather than being fully represented by options.headers / request.extensions.headers.HTTPExecutorOptions.fetch is documented as a supported WHATWG fetch implementation (index.ts:71-76) and its public type explicitly supplies context and info (index.ts:38-54). There is no documented warning that a context-dependent hook must disable deduplication, and the behaviour is not documented as caller responsibility.<!---
document/variables to the executor returned by the public entrypoint, with different context (index.ts:209-212, index.ts:329-337).fetch implementation selected from request.extensions.fetch / options.fetch is invoked as fetchFn(url, init, context, info), so it can and is intended to depend on per-request context (index.ts:220, index.ts:388-413; observed at src/index.ts:402 at the 3.3.0 tag).isCustomHeader) the library-visible headers, then inflightRequests.get(inflightRequestId) returns the existing result Promise: let inflightRequest = inflightRequests.get(inflightRequestId); (index.ts:561-570, reuse at index.ts:571-597). The map itself is module-level (index.ts:180), so it is shared across executor instances in the process.buildHTTPExecutor is exported from the package entrypoint (index.ts:182-206) and the returned executor always enters the inflight path (index.ts:624-628, index.ts:651-655). In-repository callers exist at packages/transports/http/src/index.ts:61 and packages/federation/src/supergraph.ts:1685.packages/executors/http/src/index.ts at a0d7bd84f2c655b67410831ae8119bc26c351cc4 — excerpt spanning the module-level map (line 180) and the dedup key construction and Promise reuse (lines ~561-597); ... marks elided code:
const inflightRequests = new Map<InflightRequestId, Promise<ExecutionResult>>(); // module-level: shared across executor instances
...
fetchFn(inflightRequestOptions.url, { ... }, context, info) // hook receives per-request context/info
...
let inflightRequestId = `${inflightRequestOptions.url}|${inflightRequestOptions.method}`;
if (inflightRequestOptions.body) inflightRequestId += `|${inflightRequestOptions.body}`;
if (isCustomHeader) inflightRequestId += `|${JSON.stringify(inflightRequestOptions.headers)}`; // safe sibling: library-visible headers only
let inflightRequest = inflightRequests.get(inflightRequestId); // SINK: key omits context, info, fetch identity, executor identity
if (!inflightRequest) { inflightRequest = runInflightRequest(); ... }
return handleMaybePromise(() => inflightRequest, ...);Verified dynamically against the published npm artifact (Node v24), not only by static review. A local HTTP subgraph echoed data scoped to the bearer token it received; a 200 ms upstream delay guaranteed overlap; the executor used the documented context-aware pattern, with a fetch hook attaching a per-user token taken from context.
Observed on @graphql-tools/executor-http@3.3.0: two concurrent identical queries for two different users invoked the fetch hook once, produced one upstream request carrying only the first user's Bearer token, and the second user received the first user's authorized record, including that user's sensitive per-user field.
Controls that isolate the cause:
deduplicateInflightRequests: false -> 2 hook calls, 2 upstream requests, correct per-user results.options.headers instead of the hook -> no leak, because the headers then enter the dedup key.dist) does not leak; 3.0.0 leaks; 3.3.0 leaks; 3.4.1 (latest release at time of validation) leaks.
The minimal shape of the issue, for reading alongside the above:import { buildHTTPExecutor } from '@graphql-tools/executor-http';
import { parse } from 'graphql';
let release!: () => void;
const gate = new Promise<void>(resolve => { release = resolve; });
const executor = buildHTTPExecutor({
endpoint: 'https://upstream.example/graphql',
// A real hook would add Authorization: Bearer context.token before calling fetch.
async fetch(_url, _init, context) {
await gate; // keep victim request inflight while attacker sends same query
return Response.json({ data: { viewer: context.user } });
},
});
const document = parse('query { viewer }');
const victim = executor({ document, context: { user: 'victim', token: 'victim-token' } });
const attacker = executor({ document, context: { user: 'attacker', token: 'attacker-token' } });
release();
console.log(await attacker); // { data: { viewer: 'victim' } }, not attackerfetch), or deduplication requires an explicit security-equivalence key.fetch hook runs once with the first caller's context, and the second caller receives the first caller's authorized response.Do not deduplicate across security contexts: the inflight key must either include everything the upstream authorization decision depends on, or deduplication must be skipped when that dependency is invisible to the library. The following sketch is illustrative and was not compiled or tested:
// 1. Keep inflight state per executor instance rather than module-global,
// so distinct executors (and distinct fetch hooks) never share entries.
export function buildHTTPExecutor(options?: HTTPExecutorOptions): ... {
const inflightRequests = new Map<InflightRequestId, Promise<ExecutionResult>>();
...
}
// 2. Only deduplicate when the caller has told the library what makes two
// requests security-equivalent; otherwise run each request's own fetch.
const securityKey = options?.inflightRequestSecurityKey?.(request); // new, caller-provided
const canDeduplicate = deduplicateInflightRequests && (securityKey !== undefined || !customFetchCanSeeContext);
if (canDeduplicate && securityKey !== undefined) {
inflightRequestId += `|${securityKey}`;
}An alternative, more conservative form in the same idiom: leave the key as it is, but default deduplicateInflightRequests to false whenever a custom fetch is supplied via options.fetch or request.extensions.fetch, and require an explicit opt-in to deduplicate in that case.
Notes:
isCustomHeader only adds headers the library has already materialized (index.ts:568-570); headers or credentials added inside the fetch hook are not represented in the key at all, so this partitioning cannot be relied on for the hook path.structuredClone of the shared result (index.ts:589-594) prevents cross-request mutation of a shared object, not disclosure: it copies an already-wrong result.options.headers / request.extensions.headers (headers enter the key), consumers passing no custom fetch, and any caller that sets deduplicateInflightRequests: false.HTTPExecutorOptions.fetch is documented as receiving context/info with no warning that a context-dependent hook must disable deduplication (index.ts:38-54, index.ts:71-76).
Suggested regression test alongside the existing tests for packages/executors/http (the exact spec file is unknown to this report): issue two concurrent, byte-identical requests (same endpoint, document and variables) with different context values through a fetch hook that returns data derived from context, and assert that the hook is invoked twice and each caller receives only its own context's data — plus a second assertion that two separate buildHTTPExecutor instances with different fetch hooks never share an inflight entry.
--->Source: NVD
Free Vulnerability Assessment
Evaluate your cloud security practices across 9 security domains to benchmark your risk level and identify gaps in your defenses.
Get a personalized demo
"Best User Experience I have ever seen, provides full visibility to cloud workloads."
"Wiz provides a single pane of glass to see what is going on in our cloud environments."
"We know that if Wiz identifies something as critical, it actually is."