jlexer: cap decode nesting depth to prevent stack-overflow DoS#439
Open
omkhar wants to merge 1 commit into
Open
jlexer: cap decode nesting depth to prevent stack-overflow DoS#439omkhar wants to merge 1 commit into
omkhar wants to merge 1 commit into
Conversation
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Decoding deeply-nested untrusted JSON causes a fatal, unrecoverable stack overflow (
fatal error: stack overflow). It affects two paths:(*Lexer).Interface()— theinterface{}decode path (mutually recursive per nesting level).type Node struct { Next *Node; Children []Node },map[string]Node, mutually-recursiveA/B) — these recurse through generatedUnmarshalEasyJSONper nesting level. Nointerface{}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, sorecover()cannot catch it — a single request can take down the process.encoding/jsoncaps 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 withruntime: 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 andinterface{}) funnels through — incrementing on{/[and decrementing on}/], and returning a normalLexerError("max nesting depth exceeded") once depth exceedsmaxNestingDepth = 10000(matchingencoding/json).SkipRecursivescans 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 regularUnmarshalerror.jlexer/lexer.go: +30 lines, no API change.go test ./...passes; a 50000-sibling-Raw()/SkipRecursive()regression confirms no depth leak across siblings.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.