Skip to content

jlexer: cap decode nesting depth to prevent stack-overflow DoS#439

Open
omkhar wants to merge 1 commit into
mailru:masterfrom
omkhar:omkhar/limit-decode-nesting-depth
Open

jlexer: cap decode nesting depth to prevent stack-overflow DoS#439
omkhar wants to merge 1 commit into
mailru:masterfrom
omkhar:omkhar/limit-decode-nesting-depth

Conversation

@omkhar

@omkhar omkhar commented Jun 27, 2026

Copy link
Copy Markdown

Problem

Decoding deeply-nested untrusted JSON causes a fatal, unrecoverable stack overflow (fatal error: stack overflow). It affects two paths:

  • (*Lexer).Interface() — the interface{} decode path (mutually recursive per nesting level).
  • Generated decoders for self-referential types (e.g. type Node struct { Next *Node; Children []Node }, map[string]Node, mutually-recursive A/B) — these recurse through generated UnmarshalEasyJSON per nesting level. No interface{} field is required.

There is no nesting-depth limit anywhere in jlexer/gen. The crash is a runtime fatal error raised before any user goroutine frame, so recover() cannot catch it — a single request can take down the process. encoding/json caps nesting at 10000 (maxNestingDepth) for exactly this reason; easyjson has no equivalent.

Reproduce (current master): unmarshalling [[[ … ]]] / {"c":{"c": … }} nested to a few million levels (~tens of MB) crashes with runtime: goroutine stack exceeds 1000000000-byte limit / fatal error: stack overflow.

Fix

Add a single nesting-depth counter in (*Lexer).consume() — the chokepoint every decode (generated and interface{}) funnels through — incrementing on {/[ and decrementing on }/], and returning a normal LexerError ("max nesting depth exceeded") once depth exceeds maxNestingDepth = 10000 (matching encoding/json). SkipRecursive scans flatly (no native recursion), so its single opening-delimiter increment is canceled to keep accounting balanced.

One hook covers both the interface{} path and all generated typed decoders. The fatal stack overflow becomes a regular Unmarshal error.

  • jlexer/lexer.go: +30 lines, no API change.
  • go test ./... passes; a 50000-sibling-Raw()/SkipRecursive() regression confirms no depth leak across siblings.
  • Default cap is generous; it could be made configurable if any consumer legitimately needs deeper nesting.

Disclosure note: there is no SECURITY.md / private reporting channel on this repo, so this is reported here in the open with the fix attached. Severity is bounded (the trigger needs ~tens of MB of nesting), so this is hardening against an unrecoverable DoS rather than a critical issue.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.

1 participant