GROOVY-12329: JsonSlurper: bound the length of a JSON number token - #2856
Merged
Conversation
A digit run past the long range is converted with BigInteger, and one with a decimal point or exponent with BigDecimal. Both conversions are superlinear in the digit count and neither token was bounded anywhere in groovy-json, so a document of a few hundred KB cost seconds of CPU: a 400,000-digit number took about two seconds on every parser variant, and doubling the digits quadrupled the time. A number token longer than maxNumberLength is now rejected with a JsonException before the conversion that would pay for it. The default of 1000 characters matches Jackson's StreamReadConstraints, so JSON is bounded consistently with the Jackson-backed YAML/TOML/CSV slurpers, and follows the shape GROOVY-12064 established for nesting depth: a per instance setter on JsonSlurper and JsonSlurperClassic, the groovy.json.maxNumberLength system property to override globally, and a value of 0 or less to disable the check. Rejecting a 400,000-digit number now takes single-digit milliseconds instead of two seconds. All four JsonParserType variants and the classic parser funnel through their own conversion, so each enforces the bound where it reads the token. The INDEX_OVERLAY and LAX parsers defer conversion to first access, so they check while decoding rather than in NumberValue: a document over the limit is rejected by the parse that read it, not by a later read of the value. This tightens what the parsers accept. A number longer than 1000 characters that parsed before is now rejected by default; callers that legitimately carry such values can raise or disable the limit.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #2856 +/- ##
==================================================
+ Coverage 70.8002% 70.8032% +0.0030%
- Complexity 36979 36993 +14
==================================================
Files 1576 1576
Lines 134792 134823 +31
Branches 24954 24955 +1
==================================================
+ Hits 95433 95459 +26
- Misses 30756 30764 +8
+ Partials 8603 8600 -3
🚀 New features to boost your workflow:
|
✅ All tests passed ✅🏷️ Commit: 34080e7 Learn more about TestLens at testlens.app/docs. |
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.
A digit run past the long range is converted with BigInteger, and one with a decimal point or exponent with BigDecimal. Both conversions are superlinear in the digit count and neither token was bounded anywhere in groovy-json, so a document of a few hundred KB cost seconds of CPU: a 400,000-digit number took about two seconds on every parser variant, and doubling the digits quadrupled the time.
A number token longer than maxNumberLength is now rejected with a JsonException before the conversion that would pay for it. The default of 1000 characters matches Jackson's StreamReadConstraints, so JSON is bounded consistently with the Jackson-backed YAML/TOML/CSV slurpers, and follows the shape GROOVY-12064 established for nesting depth: a per instance setter on JsonSlurper and JsonSlurperClassic, the groovy.json.maxNumberLength system property to override globally, and a value of 0 or less to disable the check. Rejecting a 400,000-digit number now takes single-digit milliseconds instead of two seconds.
All four JsonParserType variants and the classic parser funnel through their own conversion, so each enforces the bound where it reads the token. The INDEX_OVERLAY and LAX parsers defer conversion to first access, so they check while decoding rather than in NumberValue: a document over the limit is rejected by the parse that read it, not by a later read of the value.
This tightens what the parsers accept. A number longer than 1000 characters that parsed before is now rejected by default; callers that legitimately carry such values can raise or disable the limit.