BREW-MOTO-CVE-2026-75507 (GHSA-VJ8H-FX38-H3VC)

Vulnerability from osv_homebrew – Published: 2026-10-23 10:07 – Updated: 2026-10-23 10:07 – Source website
VLAI
Summary
GraphQL-core: Denial of service via quadratic complexity in the OverlappingFieldsCanBeMerged validation rule
Details

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 specified_rules 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

import time
from graphql import build_schema, parse, validate

schema = build_schema(
    "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.perf_counter()
validate(schema, document)      # returns no errors — it just burns CPU
print(f"{(time.perf_counter() - start):.0f}s")

Measured on GraphQL-core 3.2.8 / CPython 3.12 / Linux x86_64:

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 MAX_FIELD_COMPARISONS (default 250_000) 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:

from graphql.validation.rules import overlapping_fields_can_be_merged

overlapping_fields_can_be_merged.MAX_FIELD_COMPARISONS = 1_000_000

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.


{
  "affected": [
    {
      "ecosystem_specific": {
        "fix": "bump",
        "range_state": "fixed",
        "resource": "graphql-core",
        "resource_purl": "pkg:pypi/graphql-core@3.3.0",
        "upstream_fixed_in": "3.3.0rc1"
      },
      "package": {
        "ecosystem": "Homebrew",
        "name": "moto",
        "purl": "pkg:brew/moto"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "2.3.2"
            },
            {
              "fixed": "5.2.3_1"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "database_specific": {
    "confidence": "high",
    "source": "matched",
    "strategy": "registry",
    "upstream_evidence": [
      {
        "ecosystem": "PyPI",
        "key": "pkg:pypi/graphql-core@3.3.0",
        "name": "graphql-core",
        "resource": "graphql-core",
        "strategy": "registry",
        "subject_version": "3.3.0"
      }
    ]
  },
  "details": "### Summary\n\nThe `OverlappingFieldsCanBeMerged` validation rule can be driven into quadratic \u2014 and\nin the worst case `O(n\u00b2\u00b7m\u00b2)` \u2014 running time by a small, highly compressible query. A\nsingle 364 KB document can occupy one CPU core for over five minutes during\n*validation*, before any resolver runs. Because the rule is part of `specified_rules`\nand therefore enabled by default, every GraphQL-core based server is affected at\ndefault settings.\n\n### Details\n\n`OverlappingFieldsCanBeMerged` compares every pair of fields that share a response\nname and recurses into their sub-selections. The memoization added for\nCVE-2023-26144 (`GHSA-9pv7-vfvm-6vr7`) is keyed by **named fragment** identity, so it\ndoes not cover this path: inline fragments are flattened into the parent selection\nset before the cache is consulted, and therefore every pair is re-compared from\nscratch at every nesting level.\n\nRepeating an inline fragment that selects the same response name at two nesting\nlevels multiplies the two effects. There was no comparison budget and no validation\ntimeout, so the rule ran to completion regardless of cost.\n\nThe cost is incurred in the **validation phase**, which is what makes this hard to\ndefend against from outside the library: query-depth limiters, complexity analyzers\nand cost estimators all operate on the parsed document *after* validation, and the\nmalicious document is trivially shallow (depth 3, complexity 1). The payload is also\none short string repeated many times, so it compresses to a few kilobytes and slips\npast body-size limits and WAF inspection that only see the compressed request.\n\n### Proof of concept\n\n```python\nimport time\nfrom graphql import build_schema, parse, validate\n\nschema = build_schema(\n    \"type Query { field: Node } type Node { f: Node, g: Node, x: String }\"\n)\n\ndef gen(n: int, m: int) -\u003e str:\n    inner = \" \".join([\"... on Node { x }\"] * m)\n    outer = \" \".join([f\"... on Node {{ f {{ {inner} }} }}\"] * n)\n    return f\"{{ field {{ {outer} }} }}\"\n\nquery = gen(200, 100)          # 364 KB, gzips to a few KB\ndocument = parse(query)\nstart = time.perf_counter()\nvalidate(schema, document)      # returns no errors \u2014 it just burns CPU\nprint(f\"{(time.perf_counter() - start):.0f}s\")\n```\n\nMeasured on GraphQL-core 3.2.8 / CPython 3.12 / Linux x86_64:\n\n| n | m | query size | validation time |\n|---|---|---|---|\n| 20 | 20 | 7.6 KB | 0.16 s |\n| 50 | 50 | 46 KB | 5.2 s |\n| 100 | 100 | 182 KB | 81 s |\n| 200 | 100 | 364 KB | **319 s** |\n\nDoubling `n` at fixed `m` quadruples the time, as expected for a quadratic outer loop\ndriving a quadratic inner walk.\n\n### Impact\n\nDenial of service. One request pins one worker process for minutes; because CPython\nholds the GIL for this pure-CPU work, a handful of concurrent requests can saturate a\ntypical multi-worker deployment. No authentication is required, and no data is\ndisclosed or modified.\n\n### Patches\n\nFixed in **3.2.12** and **3.3.0rc1**.\n\nValidation now enforces a per-document comparison budget. Once\n`MAX_FIELD_COMPARISONS` (default `250_000`) pairwise comparisons have been made, the\nrule aborts and reports a `GraphQLError` instead of running to completion:\n\n\u003e Query is too complex to validate for overlapping fields: exceeded 250000 field\n\u003e comparisons. Validation was aborted.\n\nThe default is far above what any realistic query reaches. It is a module-level\nconstant and can be raised or lowered:\n\n```python\nfrom graphql.validation.rules import overlapping_fields_can_be_merged\n\noverlapping_fields_can_be_merged.MAX_FIELD_COMPARISONS = 1_000_000\n```\n\nNote the behaviour change: a pathological query that previously validated \u2014 slowly \u2014\nnow produces a validation error.\n\n### Workarounds\n\nIf you cannot upgrade, reject oversized documents before they reach `validate()`.\nNote that a request body-size limit must be applied to the **decompressed** body to be\neffective here, since the payload is highly compressible. Removing\n`OverlappingFieldsCanBeMergedRule` from the rule set is *not* a safe workaround: it is\na correctness rule required by the specification.\n\nA body-size limit remains worthwhile after upgrading \u2014 the comparison budget bounds\nthis specific rule, not overall request cost.\n\n### Credits\n\nReported by @BZHunt, who also reviewed the fixes\nbefore release. In memory of ZaX, who was the driving force behind this discovery.\n\nIndependently found and diagnosed by @bcmyguest, who\nalso proposed a fix.",
  "id": "BREW-moto-CVE-2026-75507",
  "modified": "2026-10-23T10:07:03Z",
  "published": "2026-10-23T10:07:03Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/graphql-python/graphql-core/security/advisories/GHSA-vj8h-fx38-h3vc"
    },
    {
      "type": "WEB",
      "url": "https://github.com/graphql-python/graphql-core/commit/150e59814d97d12140be005a558c93188ef89198"
    },
    {
      "type": "WEB",
      "url": "https://github.com/graphql-python/graphql-core/commit/29c2753a7ee30a96a4109e7c6add1a2dae0497d7"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/graphql-python/graphql-core"
    },
    {
      "type": "WEB",
      "url": "https://github.com/graphql-python/graphql-core/releases/tag/v3.2.12"
    },
    {
      "type": "WEB",
      "url": "https://github.com/graphql-python/graphql-core/releases/tag/v3.3.0rc1"
    }
  ],
  "schema_version": "1.7.3",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H",
      "type": "CVSS_V3"
    }
  ],
  "summary": "GraphQL-core: Denial of service via quadratic complexity in the OverlappingFieldsCanBeMerged validation rule",
  "upstream": [
    "GHSA-vj8h-fx38-h3vc",
    "CVE-2026-75507"
  ]
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.