Repository navigation
Bound what request validation and JSON reading cost (B03) - #18
Closed
AsyncAssassin wants to merge 2 commits into
Closed
AsyncAssassin wants to merge 2 commits into
AsyncAssassin wants to merge 2 commits into
Conversation
Validation of POST /api/v1/observed-events parsed amount into a BigDecimal even when the string broke its 80-character limit: the validator checks every constraint, and the parse takes time quadratic in the length. A million-digit amount held a request thread for about ten seconds before the 400, and Jackson accepted strings of up to 20 million characters. The amount check now leaves an over-long value to @SiZe alone. The application's ObjectMapper caps strings at 100 000 characters, far above the longest legitimate one, a provider cursor of at most 4096 by default; a longer string fails a request with 400 invalid-request and a bridge page as malformed JSON.
Review of the amount fix found the same cost next to it. The non-blank pattern of externalRef and label backtracked quadratically on a long value that ends in a line break, and validation ran it after @SiZe had failed: at the new 100 000-character cap it held a request thread for about ten seconds. It now lets the dot cross line breaks, which makes it linear; a value with a line break is no longer reported as blank. The string cap reached JSON only. Spring MVC also read application/yaml bodies, because springdoc brings the YAML data format, with no limit; that converter is removed, so YAML gets 415 as the docs always said. Request and bridge page DTOs skip unknown fields instead of buffering them until the known ones are complete, which held many times a body's size in heap and made the cap depend on key order. A hit on the cap now says so, in the API detail and in the bridge page error. The amount parse refuses an over-long value itself, so a caller that skips validation cannot reach the quadratic parse. The getters that leave a blank value to @notblank use its definition of blank, so a value of Unicode spaces gets validation-failed instead of passing validation and failing later without the errors list.
This was referenced Sep 23, 2026
Owner
Author
|
Combined into #23 together with the other batch-1 fixes, so they merge without a rebase per PR. This description keeps the detailed evidence for its fix. |
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.
Summary
B03 from the merged review of v0.4.0; 0.4.1 is released once all fixes are merged. Validation of
POST /api/v1/observed-eventsparsedamountinto aBigDecimaleven when the string broke its 80-character limit: Hibernate Validator checks every constraint, and the parse takes time quadratic in the length. A million-digitamountheld a request thread for about ten seconds before the400, and Jackson accepted strings of up to 20 million characters. AnyOPERATORcould trigger it, and anyone inlocal.@Sizealone, and the parse itself refuses such a value, so a caller that skips validation cannot reach it either.ObjectMapperaccepts strings of at most 100 000 characters instead of Jackson's 20 million. Every field that reads a string already had a far lower limit, so no valid request or bridge page is refused. A hit on the limit says so, in the API detail (400 invalid-request) and in the bridge page error.The regression review of the first version (
/code-review) found the same cost next to it, fixed in the second commit:externalRefandlabel: their non-blank pattern.*\S.*backtracked quadratically on a long value that ends in a line break, and validation ran it after@Sizehad failed. The pattern is now(?s).*\S.*, which is linear. A value with a line break is no longer reported as blank; control characters stay with B27.application/yaml, because springdoc brings the YAML data format, with none of the JSON limits. The converter is removed, so YAML gets415asdocs/architecture.mdalready said.@NotBlankused Kotlin'sisBlank(), while@NotBlankuses Java'strim(), so anamount,direction, orstatusof U+00A0 passed validation and failed later asinvalid-requestwithout the errors list.Other amount parsers need no change: the Alchemy mapper accepts at most 64 hex digits before it parses, and Jackson bounds the bridge's
BigDecimalto 1000 characters, verified for both a JSON number and a string.docs/api.md, the bridge page contract indocs/architecture.md,docs/failure-modes.md, anddocs/testing.mddescribe the limits.CHANGELOG.mdlists the changes under[Unreleased]→ Security and Fixed.Verification
./gradlew clean check: 284 tests, 0 failures, 2 skipped (the env-gated Alchemy live smoke).RequestDtoValidationTestsvalidates the DTOs directly under preemptive one-second timeouts:amountgets one length violation;externalReforlabelending in a line break gets one length violation;Integration tests:
amountgets only the length error;invalid-requestwith the limit detail;415and creates nothing.The bridge test builds its mapper with the application's customizer: unknown 100 001-character fields in the page and in an event are skipped, and a 100 001-character
nextCursorfails the page with the limit message.Mutation check: removing each fix (YAML converter,
(?s),ignoreUnknownon the request and on the page, the Unicode blank test, the parse guard) turns its test red.Live
localrun over HTTP with a 256 MiB heap, themainjar against this branch:mainamountof 100 000 digits400after 0.27 s, errorsamountValidandamount400after 0.10 s, erroramountonlyamountof 1 000 000 digits400after 11.9 s400 invalid-requestafter 0.005 sexternalRefof 100 000 characters ending in\n400after 22.2 s, two errors400after 0.04 s, length error onlyPOST /api/v1/accounts201, account created415 unsupported-media-typeexternalRef500,OutOfMemoryErrorin the log201after 0.11 samountof U+00A0400 invalid-request, no errors list400 validation-failed,amountValidThis PR and #15, #16, and #17 each add a section under
[Unreleased]inCHANGELOG.md, so each later merge needs a one-file rebase. The other files merge cleanly with all three.