Uh oh!
There was an error while loading. Please reload this page.
Bound multipart text/plain field materialization in Jersey AppSec - #11944
Conversation
Jersey 2/3 AppSec instrumentation read text/plain multipart field values via the unbounded getValue() and accumulated an unlimited number of distinct field names, allowing a crafted multipart request to exhaust heap/CPU before WAF limits apply. Reuse the existing byte-capped MultipartContentDecoder and the configured file-content limits (MAX_CONTENT_BYTES, MAX_FILES_TO_INSPECT) to bound both the size of each field value and the number of distinct field names collected.
jandro996
commented
Jul 14, 2026
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:124e9188ab
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
🎯 Code Coverage (details) 🔗 Commit SHA: d11f0a2 | Docs | Datadog PR Page | Give us feedback! |
🟢 Java Benchmark SLOs — All performance SLOs passed
PR vs. master results
Commit: Load and DaCapo benchmarks can be triggered manually in the GitLab pipeline. Results will appear in the Benchmarking Platform UI after completion. |
…d getName() - Cap now bounds total accumulated values in the body map, not just distinct field names, closing a bypass where repeating a field name skipped the limit. - Derive the body map key from FormDataContentDisposition.getName() (a safe field accessor) instead of FormDataBodyPart.getName(), which re-parses the disposition header and can throw on malformed input. Addresses Codex review findings 1 and 3 on PR #11944.
…charset Charset.defaultCharset() depends on the JVM/platform locale and can differ from the UTF-8 default Jersey's own getValue() used, corrupting text captured by AppSec on JVMs whose platform charset isn't UTF-8. Addresses Codex review finding 2 on PR #11944.
jandro996
commented
Jul 14, 2026
@codex review |
Codex Review: Didn't find any major issues. You're on a roll. Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
…latform charset" This reverts commit e25666a.
…ultipartContentDecoder MultipartContentDecoder is shared by Tomcat, Netty, Vert.x, RESTEasy and commons-fileupload. Its Charset.defaultCharset() fallback was a deliberate, reviewed decision (Manuel, PR #11198) - changing it in e25666a silently altered production behavior for all those integrations, not just Jersey. Revert the shared decoder change and instead default to UTF-8 only in Jersey's MultiPartHelper, matching Jersey's own getValue() default, by appending charset=UTF-8 to the contentType before calling the shared decoder.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:06683b8439
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
More details
Validated charset defaulting and cap boundary: contentTypeWithDefaultUtf8 correctly appends UTF-8 when no charset is declared and preserves explicit charsets unchanged; the totalBodyMapValues cap blocks ALL additions once the total reaches MAX_FILES_TO_INSPECT — including repeated field names — which is more restrictive than the PR description's "distinct-field-name" framing, but intentional per the in-code comment and test assertions.
📊 Validated against 28 scenarios · Open Bits AI session
🤖 Datadog Autotest · Commit 06683b8 · What is Autotest? · Any feedback? Reach out in #autotest
readContent() served both the bodyMap (text field) and filesContent call sites, so the Jersey-local UTF-8 default leaked into file content too. Split it into readContent() (UTF-8 default, text fields) and readFileContent() (unmodified content type, parity with MultipartContentDecoder's fallback used by file content in every other multipart helper).
Uh oh!
There was an error while loading. Please reload this page.
/merge |
View all feedbacks in Devflow UI.
The expected merge time in
|
Uh oh!
There was an error while loading. Please reload this page.
What Does This Do
bodyPart.getValue()call inMultiPartHelper.collectBodyPart(jersey2 and jersey3 AppSec modules) withreadContent(bodyPart), which reads the part through the existing byte-cappedMultipartContentDecoder(bounded byMAX_CONTENT_BYTES, backed byConfig.get().getAppSecMaxFileContentBytes()).bodyMap(not just distinct field names) atMAX_FILES_TO_INSPECT, symmetric to the existing cap onfilesContent. Repeating the same field name across multiple parts no longer bypasses the limit — every collected value counts against the cap.bodyMapkey viaFormDataContentDisposition.getName()(a safe field accessor obtained through the existing malformed-disposition guard) instead of the unguardedbodyPart.getName(), which re-parses the disposition header and can throw on malformed input outside that guard.readContent()(used only by thebodyMap/text-field path, defaults undeclared charset to UTF-8 to match Jersey's owngetValue()behavior) andreadFileContent()(used only by thefilesContentpath, keepsMultipartContentDecoder's own charset fallback for parity with file content in every other multipart helper). The UTF-8 default is applied locally in Jersey's helper only — the sharedMultipartContentDecoderfallback (Charset.defaultCharset()) is untouched, since it's also used by Tomcat, Netty, Vert.x, RESTEasy and commons-fileupload.dd-java-agent/instrumentation/jersey/jersey-appsec/jersey-appsec-2.0(javax.ws.rs) andjersey-appsec-3.0(jakarta.ws.rs) — the twoMultiPartHelper.javafiles remain byte-for-byte identical modulo the namespace.MultiPartHelperTest.groovyin both modules to mockbodyPart.getEntityAs(InputStream)instead ofbodyPart.getValue(), and adds test cases for: value truncation beyondMAX_CONTENT_BYTES, the total-value cap (MAX_FILES_TO_INSPECT) blocking further accumulation regardless of distinct field names, and charset defaulting for undeclared-charset text fields vs. file content.Motivation
Jersey 2/3 AppSec instrumentation materialized
text/plainmultipart field values via the unboundedgetValue()and accumulated an unlimited number of values into the body map before any WAF limit was applied. A crafted multipart request with a very largetext/plainfield, or a large number of parts (even repeating the same field name), could exhaust heap/CPU before hitting any bound.Additional Notes
MultipartFormDataReaderInstrumentationhas a related unbounded-read pattern (getBodyAsString()on all parts) that is out of scope for this PR and should be addressed as a follow-up.text/plainfields went through a short-lived regression: an early attempt changed the sharedMultipartContentDecoder's fallback to UTF-8 directly, which would have silently altered production behavior for Tomcat, Netty, Vert.x, RESTEasy and commons-fileupload (that fallback isCharset.defaultCharset()by deliberate design, per PR Add server.request.body.files_content AppSec address for Tomcat and Netty multipart #11198). This was reverted; the UTF-8 default is now applied only within Jersey's own helper.Contributor Checklist
type:and (comp:orinst:) labels in addition to any other useful labelsclose,fix, or any linking keywords when referencing an issueUse
solvesinstead, and assign the PR milestone to the issueJira ticket: APMSP-3237
Note:Once your PR is ready to merge, add it to the merge queue by commenting
/merge./merge -ccancels the queue request./merge -f --reason "reason"skips all merge queue checks; please use this judiciously, as some checks do not run at the PR-level. For more information, see this doc.