Skip to content

parser: bound nesting depth to prevent stack-overflow DoS - #516

Merged
StevenACoffman merged 2 commits into
vektah:masterfrom
amanverma-wsu:fix/parser-selection-set-depth-limit
Oct 22, 2026
Merged

StevenACoffman merged 2 commits into
vektah:masterfrom
amanverma-wsu:fix/parser-selection-set-depth-limit

Conversation

@amanverma-wsu

@amanverma-wsu amanverma-wsu commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Problem: Unlimited parser token limit also have unlimited recursion depth stack overflow

Note: Users have already been recommended to set a parser token limit if there is any risk of malicious input, since CVE-2023-49559 introduced the ability to do so. Anyone who sets gqlparser's parser token limit to 15000 or less should be unaffected by this new issue, gqlgen's srv.SetParserTokenLimit(15000). However, the gqlparser default token limit of 0 signifies unlimited, which is only suitable for local development or internal projects.

The parser recurses once per nesting level and places no limit on depth. Deeply nested input exhausts the goroutine stack, causing fatal error: stack overflow. That is a runtime throw, not a panic, so recover() cannot catch it and the whole process exits.

Every recursive construct is affected, not only selection sets. With default ParseQuery (go1.27, 64-bit, 1 GB stack):

Input Crashes at Payload
Object value {a:{a:…}} ~690k levels ~2.8 MB
List value [[[…]]] (arguments, directive arguments, variable defaults) ~960k levels ~1.9 MB
Selection set {a{a{…}}} ~700k levels ~1.4 MB
List type [[[T]]] (variable types) ~2M levels ~4 MB

ParseSchema has the same problem with list defaults and list field types.

The root cause is the same as CVE-2023-49559: the parser's limits default to unlimited. A token limit does mitigate this, since the smallest crash needs about 960k tokens. But ParseQuery sets no token limit, and neither does gqlgen by default, so one unauthenticated request crashes the server before validation runs.

This was reported privately and is being coordinated for a CVE through GitHub.

Workaround for users who can't upgrade: set a parser token limit, e.g. gqlgen's srv.SetParserTokenLimit(15000). That rejects every payload above.

Change

  • Add const MaxNestingDepth = 1000. Selection sets, list and object values, and list types are counted together against it, and deeper input returns a located parse error (exceeded maximum nesting depth of 1000).
  • Guard every recursive production: parseOptionalSelectionSet (which parseRequiredSelectionSet now delegates to), parseList, parseObject, and the list branch of parseTypeReference.
  • The limit lives in the parser itself, so every entry point is covered without API changes: ParseQuery, ParseQueryWithTokenLimit, ParseSchema*, and the root LoadQuery. There's no opt-out and no new public API beyond the constant.

Behaviour change

Documents nested deeper than 1000 levels, counting all kinds together, are now rejected. Real documents nest a few dozen levels, and a 1000-deep query parses and validates in about 7 ms.

Tests

parser/nesting_test.go:

  • Table test over 11 constructs: selection set, inline fragment, fragment definition, list, object and directive arguments, variable type, variable default, mixed selection set and list, schema field type, and schema argument default. For each, depth 1000 parses, 1001 fails with the exact message and a location, and 3,000,000 returns an error instead of crashing.
  • Sibling test: fields, operations and arguments each 600 deep don't add to one another's depth.
  • Mutation-checked: removing any single guard, or the depth decrement, fails at least one test.

amanverma-wsu and others added 2 commits October 21, 2026 12:39
The first commit guarded only selection sets; list and object values
and list types still recursed without limit and crashed ParseQuery with
about 2 MB of input. Count every recursive production against a single
MaxNestingDepth of 1000 with no opt-out, report the error with a
location, and drop the new ParseQueryWithLimits / SetMaxDepthLimit API.

Also remove the gqlparser-depth-limit.patch file committed by mistake.
@StevenACoffman
StevenACoffman force-pushed the fix/parser-selection-set-depth-limit branch from 9dd2119 to 44fe9ad Compare October 22, 2026 01:30
@StevenACoffman StevenACoffman changed the title parser: bound selection-set nesting depth to prevent stack-overflow DoS parser: bound nesting depth to prevent stack-overflow DoS Oct 22, 2026
@amanverma-wsu

Copy link
Copy Markdown
Contributor Author

Confirmed your 44fe9ad on my end: I reproduced all four vectors (selection sets, list values, object values, list types) at ~2M deep against your branch and they all return “exceeded maximum nesting depth” now, no crash, with normal queries unaffected. The single shared counter is cleaner than what I had, and good catch on the list-type path. Looks complete to me. Whenever you cut a release and open the GitHub Security Advisory, happy to help with the fields for the CVE.

@StevenACoffman
StevenACoffman merged commit b530514 into vektah:master Oct 22, 2026
8 checks passed
@amanverma-wsu
amanverma-wsu deleted the fix/parser-selection-set-depth-limit branch October 22, 2026 01:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants