Skip to content

chore(deps): update dependency graphql-core to v3.2.12 [security] - #102

Merged
renovate[bot] merged 1 commit into
mainfrom
renovate/pypi-graphql-core-vulnerability
Nov 10, 2026
Merged

renovate[bot] merged 1 commit into
mainfrom
renovate/pypi-graphql-core-vulnerability

Conversation

@renovate

@renovate renovate Bot commented Nov 9, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
graphql-core (changelog) 3.2.11 → 3.2.12 age confidence

GraphQL-core: Denial of service via comment tokens bypassing the parser's max_tokens limit

CVE-2026-75508 / GHSA-r77w-qph3-7vf8

More information

Details

Summary

The parser's max_tokens limit — the documented defence against oversized GraphQL
documents — did not count comment tokens. A document padded with comments could
therefore pass a max_tokens check of any size while still allocating one Token
object per comment, all of which stay reachable for the lifetime of the parsed
document. A 38 MB comment-padded payload allocated roughly 92 MB of additional
resident memory with max_tokens=5 in force.

Details

Lexer.lookahead() reads tokens one at a time and skips over comments, returning only
the first non-comment token. Each skipped comment is nevertheless fully tokenized,
allocated as a Token object, and linked into the persistent token list via
token.next / token.prev — the chain that powers source-location reporting, and
which is retained as long as the document is.

Parser.advance_lexer() incremented the token counter only for the tokens the lexer
actually returned. Comments were therefore invisible to the accounting, and
max_tokens bounded only the non-comment tokens.

The practical consequence is a gap between what users reasonably believe max_tokens
protects and what it did protect. The limit was not broken for ordinary documents —
real tokens were still capped correctly — so the shortfall was not observable without
specifically testing comment padding.

As with the companion issue, the payload compresses extremely well (# c\n repeated),
so body-size limits and WAF inspection applied to the compressed request do not see
its true size, while the framework decompresses it in memory before the parser is
reached.

Proof of concept
import gc
import resource
from graphql import parse

##### 100,000 comments, a 5-token query, and max_tokens=5 — accepted.
query = "# c\n" * 100_000 + "{ a }"
parse(query, max_tokens=5)          # no error raised

##### Memory amplification.
big = "# padding comment used to amplify memory per token\n" * 500_000 + "{ a }"
gc.collect()
before = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
parse(big, max_tokens=5)            # ~38 MB source
after = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss
print(f"RSS delta: +{(after - before) / 1024:.0f} MB")

Measured on GraphQL-core 3.2.8 / CPython 3.12 / Linux x86_64: the 391 KB payload
parsed in 127 ms, and the 38.1 MB payload produced an RSS delta of +92 MB — about
2.4× the source size, on top of the source string itself.

Impact

Denial of service through memory exhaustion. A modest number of concurrent requests
can push a worker past a typical 256 MB or 512 MB container limit and trigger an
OOM kill. No authentication is required, and no data is disclosed or modified.

Depth limiters and complexity analyzers do not help: they run on the AST after parsing
has already succeeded, and the document contains only five real tokens.

Patches

Fixed in 3.2.12 and 3.3.0rc1.

Parser.advance_lexer now counts every token consumed, including the comment tokens
the lexer skips between the previously focused token and the new one, and including
the advance onto EOF so that trailing comments are covered. max_tokens therefore now
bounds every Token the parser allocates.

Note the behaviour change: comments now count toward max_tokens. A
comment-heavy document that was previously accepted may now be rejected. If you parse
documents containing substantial comments — SDL files with documentation comments, for
instance — and you set max_tokens, you may need to raise the limit. The
token_count attribute of a parsed document likewise now includes comments.

Workarounds

If you cannot upgrade, apply a request body-size limit to the decompressed body
before the document reaches parse().

Such a limit is worth keeping after upgrading as well: max_tokens is one guard among
several and bounds the token count, not the overall cost of a request.

References
  • GraphQL specification § 2.1.4 — Comments
Credits

Reported by @​BZHunt, who also reviewed the fixes
before release. In memory of ZaX, who was the driving force behind this discovery.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


GraphQL-core: Denial of service via quadratic complexity in the OverlappingFieldsCanBeMerged validation rule

CVE-2026-75507 / GHSA-vj8h-fx38-h3vc

More information

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.

Severity

  • CVSS Score: 7.5 / 10 (High)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

graphql-python/graphql-core (graphql-core)

v3.2.12

Compare Source

Patch-release GraphQL-core v3.2.12, based on GraphQL.js v16.14.2.

This patch-release supports Python 3.7 to 3.14.

This is a security release. It fixes two denial-of-service vulnerabilities in the validator and the parser. Upgrading is recommended for all users.

Security fixes:

  • CVE-2026-75507 (GHSA-vj8h-fx38-h3vc, High) — the OverlappingFieldsCanBeMerged validation rule could be driven into quadratic and worse running time by a small, highly compressible query using repeated inline fragments, occupying a worker for minutes of CPU during validation, before any resolver runs. Validation now enforces a per-document comparison budget and aborts with a GraphQLError once it is exhausted.
  • CVE-2026-75508 (GHSA-r77w-qph3-7vf8, High) — comment tokens were allocated and retained but not counted toward the parser's max_tokens limit, so a comment-padded document could bypass the limit while allocating one token object per comment. max_tokens now counts every token the parser consumes, including skipped comments.

Please note the two behaviour changes this implies:

  • A pathological query that previously validated — slowly — now produces a validation error. Realistic queries stay far below the limit, which defaults to 250,000 field comparisons and can be adjusted by assigning to graphql.validation.rules.overlapping_fields_can_be_merged.MAX_FIELD_COMPARISONS.
  • Comments now count toward max_tokens, so a comment-heavy document that was previously accepted may now be rejected. If you parse documents with substantial comments and set max_tokens, you may need to raise the limit. The token_count of a parsed document now includes comments as well.

Both limits complement, but do not replace, a request body-size limit applied to the decompressed request body in your web framework.

Other changes:

  • Updated development dependencies and CI tooling.

The security issues were reported by @​BZHunt, who also reviewed the fixes before release. In memory of ZaX, who was the driving force behind this discovery. The first one was also independently found and diagnosed by @​bcmyguest.

Thanks to @​jkimbo for sponsoring this project.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Enabled.

♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added the dependencies label Nov 9, 2026
@renovate
renovate Bot merged commit ede8941 into main Nov 10, 2026
6 checks passed
@renovate
renovate Bot deleted the renovate/pypi-graphql-core-vulnerability branch November 10, 2026 02:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants