Skip to content

Bound multipart text/plain field materialization in Jersey AppSec - #11944

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 7 commits into
masterfrom
APMSP-3237
Jul 24, 2026
Merged

Bound multipart text/plain field materialization in Jersey AppSec#11944
gh-worker-dd-mergequeue-cf854d[bot] merged 7 commits into
masterfrom
APMSP-3237

Conversation

@jandro996

@jandro996jandro996 commented Jul 14, 2026

Copy link
Copy Markdown
Member

What Does This Do

  • Replaces the unbounded bodyPart.getValue() call in MultiPartHelper.collectBodyPart (jersey2 and jersey3 AppSec modules) with readContent(bodyPart), which reads the part through the existing byte-capped MultipartContentDecoder (bounded by MAX_CONTENT_BYTES, backed by Config.get().getAppSecMaxFileContentBytes()).
  • Caps the total number of values accumulated across the bodyMap (not just distinct field names) at MAX_FILES_TO_INSPECT, symmetric to the existing cap on filesContent. Repeating the same field name across multiple parts no longer bypasses the limit — every collected value counts against the cap.
  • Derives the bodyMap key via FormDataContentDisposition.getName() (a safe field accessor obtained through the existing malformed-disposition guard) instead of the unguarded bodyPart.getName(), which re-parses the disposition header and can throw on malformed input outside that guard.
  • Splits the charset-handling helper into readContent() (used only by the bodyMap/text-field path, defaults undeclared charset to UTF-8 to match Jersey's own getValue() behavior) and readFileContent() (used only by the filesContent path, keeps MultipartContentDecoder'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 shared MultipartContentDecoder fallback (Charset.defaultCharset()) is untouched, since it's also used by Tomcat, Netty, Vert.x, RESTEasy and commons-fileupload.
  • Applies all of the above identically to both dd-java-agent/instrumentation/jersey/jersey-appsec/jersey-appsec-2.0 (javax.ws.rs) and jersey-appsec-3.0 (jakarta.ws.rs) — the two MultiPartHelper.java files remain byte-for-byte identical modulo the namespace.
  • Updates MultiPartHelperTest.groovy in both modules to mock bodyPart.getEntityAs(InputStream) instead of bodyPart.getValue(), and adds test cases for: value truncation beyond MAX_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/plain multipart field values via the unbounded getValue() and accumulated an unlimited number of values into the body map before any WAF limit was applied. A crafted multipart request with a very large text/plain field, or a large number of parts (even repeating the same field name), could exhaust heap/CPU before hitting any bound.

Additional Notes

  • Scope is intentionally limited to Jersey. RESTEasy's MultipartFormDataReaderInstrumentation has 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.
  • The charset default for undeclared-charset text/plain fields went through a short-lived regression: an early attempt changed the shared MultipartContentDecoder's fallback to UTF-8 directly, which would have silently altered production behavior for Tomcat, Netty, Vert.x, RESTEasy and commons-fileupload (that fallback is Charset.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

Jira ticket: APMSP-3237

Note:Once your PR is ready to merge, add it to the merge queue by commenting /merge./merge -c cancels 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.

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

Copy link
Copy Markdown
MemberAuthor

@codex review

@jandro996jandro996 added type: bug fix Bug fix comp: asm waf Application Security Management (WAF) labels Jul 14, 2026

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

@datadog-datadog-us1-prod

datadog-datadog-us1-prodBot commented Jul 14, 2026

Copy link
Copy Markdown

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 69.73% (+12.53%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: d11f0a2 | Docs | Datadog PR Page | Give us feedback!

@dd-octo-sts

dd-octo-stsBot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

🟢 Java Benchmark SLOs — All performance SLOs passed

SuiteStatus
Startup🟢 pass

SLO thresholds are defined here based on automatically generated metrics. A warning is raised when results are within 5% of the threshold.

PR vs. master results
ScenarioCandidatemasterΔ (95% CI of mean)
startup:insecure-bank:iast:Agent13.99 s13.98 s[-0.8%; +0.9%] (no difference)
startup:insecure-bank:tracing:Agent12.90 s12.98 s[-1.3%; +0.0%] (no difference)
startup:petclinic:appsec:Agent16.98 s16.87 s[-0.4%; +1.7%] (no difference)
startup:petclinic:iast:Agent17.04 s17.04 s[-0.8%; +0.7%] (no difference)
startup:petclinic:profiling:Agent16.80 s16.83 s[-1.3%; +0.9%] (no difference)
startup:petclinic:sca:Agent17.02 s16.77 s[+0.6%; +2.4%] (maybe worse)
startup:petclinic:tracing:Agent15.76 s16.11 s[-6.4%; +2.0%] (no difference)

Commit:d11f0a24 · CI Pipeline · Benchmarking Platform UI


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

Copy link
Copy Markdown
MemberAuthor

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. You're on a roll.

Reviewed commit:e25666acf5

ℹ️ 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".

…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.
@jandro996
jandro996 marked this pull request as ready for review July 14, 2026 13:28
@jandro996
jandro996 requested a review from a team as a code ownerJuly 14, 2026 13:28
@jandro996
jandro996 requested review from ValentinZakharov, claponcet, manuel-alvarez-alvarez and vandonr and removed request for a teamJuly 14, 2026 13:28

@chatgpt-codex-connectorchatgpt-codex-connectorBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

@datadog-datadog-us1-proddatadog-datadog-us1-prodBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Datadog Autotest: PASS

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.

Was this helpful? React 👍 or 👎

📊 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).
@jandro996
jandro996 added this pull request to the merge queueJul 24, 2026
@dd-octo-sts

Copy link
Copy Markdown
Contributor

/merge

@gh-worker-devflow-routing-ef8351

gh-worker-devflow-routing-ef8351Bot commented Jul 24, 2026

Copy link
Copy Markdown

View all feedbacks in Devflow UI.

2026-07-24 11:38:35 UTC ℹ️ Start processing command /merge


2026-07-24 11:38:40 UTC ℹ️ MergeQueue: pull request added to the queue

The expected merge time in master is approximately 1h (p90).


2026-07-24 12:33:09 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Jul 24, 2026
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot merged commit 84de0a8 into masterJul 24, 2026
586 checks passed
@gh-worker-dd-mergequeue-cf854d
gh-worker-dd-mergequeue-cf854dBot deleted the APMSP-3237 branch July 24, 2026 12:33
@github-actionsgithub-actionsBot added this to the 1.65.0 milestone Jul 24, 2026
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: asm wafApplication Security Management (WAF)type: bug fixBug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@jandro996@manuel-alvarez-alvarez