GHSA-fc86-6rv6-2jpm: Webonyx/graphql php Webonyx/graphql-php Has Quadratic Validation Cost Vulnerability
HIGHGHSA-fc86-6rv6-2jpm is a HIGH (CVSS 7.5) webonyx/graphql-php has quadratic validation cost vulnerability (CWE-407: Inefficient Algorithmic Complexity) affecting Webonyx/graphql php libraries in the Packagist ecosystem. The issue involves webonyx/graphql-php has quadratic validation cost in overlappingfieldscanbemerged via inline fragments. Remediation is available by upgrading affected packages to version 15.32.2.
GHSA-fc86-6rv6-2jpm at a Glance
Security Impact
CVSS: 7.5
The security impact metrics below are parsed directly from the CVSS v3.1 vector string (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H).
How to Fix GHSA-fc86-6rv6-2jpm
To remediate this vulnerability, upgrade all affected Webonyx/graphql php dependencies to a patched release. Both direct and transitive dependencies should be checked, as frameworks or security libraries may pull in vulnerable versions transitively.
Upgrade webonyx/graphql-php to 15.32.2
Run the following command in your project directory to verify that all resolved dependencies use the patched release:
inspect dependencies for webonyx/graphql-php
How to Check if You Are Affected
To determine whether your application is affected by GHSA-fc86-6rv6-2jpm, inspect your project dependencies for Webonyx/graphql php:
inspect dependencies for webonyx/graphql-php
Analyze the terminal output to locate any version of Webonyx/graphql php in your dependency tree. If any resolved version matches the vulnerable ranges listed in the table below (for example, versions earlier than 15.32.2), your application is affected.
Note: Verify declared build manifests and deployed runtime container environments.
Technical Details
Vulnerability Mechanism & Technical Analysis
This vulnerability is classified under CWE-407: Inefficient Algorithmic Complexity, where an algorithm in a product has an inefficient worst-case computational complexity that may be detrimental to system performance and can be triggered by an attacker, typically using crafted manipulations that ensure that the worst case is being reached. In Webonyx/graphql php, software state validation or input boundaries are circumvented during processing. Review the official source advisory statement below for specific affected components, classes, and reproduction details.
Source Advisory Statement
GHSA-fc86-6rv6-2jpm Affected Webonyx/graphql php Versions
1 component(s)| Product / Package | Ecosystem | Affected Versions | Fixed Version |
|---|---|---|---|
|
webonyx/graphql-php
|
Packagist |
< 15.32.2
|
15.32.2
|
References
Frequently Asked Questions
What is GHSA-fc86-6rv6-2jpm?
Which Webonyx/graphql php versions are affected?
What version fixes GHSA-fc86-6rv6-2jpm?
Is webonyx/graphql-php affected?
What is the CVSS score of GHSA-fc86-6rv6-2jpm?
What CWE is associated with GHSA-fc86-6rv6-2jpm?
How can I check Packagist dependencies for GHSA-fc86-6rv6-2jpm?
Machine-readable OSV JSON
OSV v1.5 Schema
{
"schema_version" : "1.4.0",
"id" : "GHSA-fc86-6rv6-2jpm",
"modified" : "2026-05-04T22:22:09Z",
"published" : "2026-05-04T22:22:09Z",
"aliases" : [ ],
"summary" : "webonyx/graphql-php has quadratic validation cost in OverlappingFieldsCanBeMerged via inline fragments",
"details" : "## Summary\n\n`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.\n\n## Affected Component\n\n`src/Validator/Rules/OverlappingFieldsCanBeMerged.php`\n\n## Description\n\ngraphql-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.\n\ngraphql-php includes a `comparedFragmentPairs` PairSet cache (the same class of memoization fix tracked under [CVE-2023-26144 / GHSA-9pv7-vfvm-6vr7](https://github.com/advisories/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.\n\nThis finding has been tested against the **latest stable release `webonyx/graphql-php@v15.31.4`** running on PHP 8.3.30.\n\n## Root Cause\n\n### 1. Pairwise `O(n^2)` loop (`collectConflictsWithin`)\n\n```php\n// src/Validator/Rules/OverlappingFieldsCanBeMerged.php:306\n$fieldsLength = count($fields);\n\nif ($fieldsLength > 1) {\n for ($i = 0; $i < $fieldsLength; ++$i) { // line 311\n for ($j = $i + 1; $j < $fieldsLength; ++$j) { // line 312\n $conflict = $this->findConflict(\n $context,\n $parentFieldsAreMutuallyExclusive,\n $responseName,\n $fields[$i],\n $fields[$j]\n );\n // ...\n }\n }\n}\n```\n\n`count($fields)` grows without bound when multiple inline fragments select the same response name in the same parent selection set.\n\n### 2. Inline fragment flattening (`internalCollectFieldsAndFragmentNames`)\n\n```php\n// src/Validator/Rules/OverlappingFieldsCanBeMerged.php:266\ncase $selection instanceof InlineFragmentNode:\n $typeCondition = $selection->typeCondition;\n $inlineFragmentType = $typeCondition === null\n ? $parentType\n : AST::typeFromAST([$context->getSchema(), 'getType'], $typeCondition);\n\n $this->internalCollectFieldsAndFragmentNames(\n $context,\n $inlineFragmentType,\n $selection->selectionSet,\n $astAndDefs, // flattened into the parent map\n $fragmentNames\n );\n break;\n```\n\n`N` inline fragments selecting the same response name produce `N` entries in `$astAndDefs[$responseName]`, which then trigger `N*(N-1)/2` `findConflict` calls.\n\n### 3. The named-fragment cache does not cover this code path\n\n```php\n// src/Validator/Rules/OverlappingFieldsCanBeMerged.php:41\nprotected PairSet $comparedFragmentPairs;\n\n// :54 (in __construct)\n$this->comparedFragmentPairs = new PairSet();\n```\n\n`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.\n\n### 4. No comparison budget, no validation timeout\n\nThere 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.\n\n## Proof of Concept\n\n```php\n<?php\n// composer require webonyx/graphql-php:v15.31.4\nrequire __DIR__.'/vendor/autoload.php';\n\nuse GraphQL\\Language\\Parser;\nuse GraphQL\\Validator\\DocumentValidator;\nuse GraphQL\\Utils\\BuildSchema;\n\n$schema = BuildSchema::build('type Query { field: Node } type Node { f: Node, g: Node, x: String }');\n\nfunction gen(int $n, int $m): string {\n $inner = implode(' ', array_fill(0, $m, '... on Node { x }'));\n $outer = implode(' ', array_fill(0, $n, \"... on Node { f { $inner } }\"));\n return \"{ field { $outer } }\";\n}\n\necho \" N M | size | validate ms | errors\\n\";\necho \"---------|-----------|-------------|--------\\n\";\nforeach ([[20,20],[50,50],[100,50],[100,100],[150,100],[200,100]] as [$n, $m]) {\n $q = gen($n, $m);\n $doc = Parser::parse($q);\n $t0 = microtime(true);\n $errors = DocumentValidator::validate($schema, $doc);\n $elapsed = round((microtime(true) - $t0) * 1000);\n printf(\"%4d %4d | %7dB | %10d | %d\\n\", $n, $m, strlen($q), $elapsed, count($errors));\n}\n```\n\n### Measured output on `webonyx/graphql-php@v15.31.4`, PHP 8.3.30, Linux x86_64\n\n```\ngraphql-php version: v15.31.4\nPHP version: 8.3.30\n\n N M | size | validate ms | errors\n---------|-----------|-------------|--------\n 20 20 | 7653B | 71 | 0\n 50 50 | 46113B | 2020 | 0\n 100 50 | 92213B | 7762 | 0\n 100 100 | 182213B | 29660 | 0\n 150 100 | 273313B | 66052 | 0\n 200 100 | 364413B | 117082 | 0\n```\n\nThe 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.\n\n## Impact\n\n- **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.\n- **Pre-execution**: the cost is in the validation phase. `QueryComplexity` and `QueryDepth` rules cannot help: the example query has depth 3 and complexity 1.\n- **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.\n- **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.\n- **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.\n- **Existing CVE-2023-26144 fix is insufficient**: the published `PairSet` cache only memoizes named-fragment comparisons, not the inline-fragment flattening path.\n\nThis 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.\n\n## Affected Versions\n\n- **`webonyx/graphql-php@v15.31.4`** (latest stable as of 2026-04-08): all measurements above were collected on this version with no custom configuration.\n- 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 were not retested individually.\n\n## Remediation\n\nThree options ordered from best to minimal:\n\n### Option 1 -- Adopt the Adameit algorithm\n\nReplace pairwise comparison with the uniqueness-check algorithm designed by Simon Adameit, used today by graphql-java (post CVE-2023-28867) and Sangria. The algorithm transforms conflict-freedom into a uniqueness requirement and runs in `O(n log n)` instead of `O(n^2)`. See [graphql/graphql-js issue #2185](https://github.com/graphql/graphql-js/issues/2185) for the design discussion and the [Sangria PR #12](https://github.com/sangria-graphql-org/sangria/pull/12) for the original implementation.\n\n### Option 2 -- Comparison budget\n\nAdd a comparison counter on `OverlappingFieldsCanBeMerged` shared across `collectConflictsWithin`, `collectConflictsBetween`, and the recursive `findConflict` calls. Throw a `Error` after a configurable threshold (for example 10,000 comparisons by default). This is the approach graphql-java implemented after CVE-2023-28867.\n\n### Option 3 -- Cap inline-fragment flattening\n\nIn `internalCollectFieldsAndFragmentNames`, cap `count($astAndDefs[$responseName])` at a configurable limit (for example 1,000) and emit a validation error if exceeded. This is a narrower fix that targets the specific bypass path but does not address other potential `O(n^2)` surfaces.\n\n## Resources\n\n- [CVE-2023-26144 -- graphql-js (partial fix, named-fragment cache only)](https://github.com/advisories/GHSA-9pv7-vfvm-6vr7)\n- [CVE-2023-28867 -- graphql-java (full fix via Adameit algorithm)](https://github.com/advisories/GHSA-p4qx-6w5p-4rj2)\n- [graphql-js Issue #2185 -- Faster algorithm for OverlappingFieldsCanBeMerged](https://github.com/graphql/graphql-js/issues/2185)\n- [Sangria PR #12 -- original optimised implementation](https://github.com/sangria-graphql-org/sangria/pull/12)\n- [GraphQL spec section 5.3.2 -- Field Selection Merging](https://spec.graphql.org/October2021/#sec-Field-Selection-Merging)\n- Companion advisory for this implementation: `GraphQL\\Language\\Parser` parser stack overflow via deeply nested queries (Unbounded recursion in parser causes stack overflow on crafted nested input).",
"severity" : [ {
"type" : "CVSS_V3",
"score" : "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H"
} ],
"affected" : [ {
"package" : {
"ecosystem" : "Packagist",
"name" : "webonyx/graphql-php"
},
"ranges" : [ {
"type" : "ECOSYSTEM",
"events" : [ {
"introduced" : "0"
}, {
"fixed" : "15.32.2"
} ]
} ]
} ],
"references" : [ {
"type" : "WEB",
"url" : "https://github.com/webonyx/graphql-php/security/advisories/GHSA-fc86-6rv6-2jpm"
}, {
"type" : "WEB",
"url" : "https://github.com/graphql/graphql-js/issues/2185"
}, {
"type" : "WEB",
"url" : "https://github.com/sangria-graphql-org/sangria/pull/12"
}, {
"type" : "WEB",
"url" : "https://github.com/webonyx/graphql-php/commit/996adcfce33442f6fc01214777bc8620cc142d85"
}, {
"type" : "ADVISORY",
"url" : "https://github.com/advisories/GHSA-9pv7-vfvm-6vr7"
}, {
"type" : "ADVISORY",
"url" : "https://github.com/advisories/GHSA-p4qx-6w5p-4rj2"
}, {
"type" : "PACKAGE",
"url" : "https://github.com/webonyx/graphql-php"
}, {
"type" : "WEB",
"url" : "https://github.com/webonyx/graphql-php/releases/tag/v15.32.2"
}, {
"type" : "WEB",
"url" : "https://spec.graphql.org/October2021/#sec-Field-Selection-Merging"
} ],
"database_specific" : {
"cwe_ids" : [ "CWE-407" ],
"severity" : "HIGH",
"github_reviewed" : true,
"github_reviewed_at" : "2026-05-04T22:22:09Z",
"nvd_published_at" : null
}
}