Skip to content

Fix multipart body-map cap bypass and charset handling in RESTEasy - #12085

Open
jandro996 wants to merge 12 commits into
masterfrom
alejandro.gonzalez/APPSEC-69139-resteasy-bound-multipart
Open

Fix multipart body-map cap bypass and charset handling in RESTEasy#12085
jandro996 wants to merge 12 commits into
masterfrom
alejandro.gonzalez/APPSEC-69139-resteasy-bound-multipart

Conversation

@jandro996

@jandro996 jandro996 commented Jul 28, 2026

Copy link
Copy Markdown
Member

What Does This Do

  • Adds MultipartHelper.collectBodyMap(MultipartFormDataInput), called from MultipartFormDataReaderInstrumentation.ReadFromAdvice.after() to build the server.request.body AppSec address for RESTEasy 3.0 multipart requests.
  • Caps the map by total accumulated values (total >= MAX_FILES_TO_INSPECT) rather than by distinct field name, since getFormDataMap() already groups parts by field name — a per-key cap would be trivially bypassed by repeating the same field name on every part.
  • Filters collectBodyMap to text/plain parts with no filename attribute, matching the Jersey reference (jersey3/MultiPartHelper.collectBodyPart): file parts are reported separately via collectFilenames/collectFilesContent and must not also consume the body-map budget. A part with no Content-Type header defaults to text/plain, per InputPart's own javadoc. The filename check recognizes both the plain filename and the RFC 5987 extended filename* Content-Disposition parameters (hasFilenameParam), so a part carrying only filename* can't bypass the exclusion; collectFilesContent()'s own filename gate was updated to the same check for consistency, since it had the identical blind spot.
  • Defaults undeclared-charset body-map text values to UTF-8 (contentTypeWithDefaultUtf8), matching Jersey's own getValue() behavior, instead of falling back to MultipartContentDecoder's JVM-platform-charset default — which is still used as-is for collectFilesContent() (binary file content), left unchanged. The null-content-type fallback synthesizes a full "text/plain; charset=UTF-8" value (not a bare "charset=UTF-8" parameter) so it parses correctly under extractCharset().
  • Widens the per-part read/header-extraction exception handling in readContent/contentTypeOf from catch (IOException) to catch (Exception) with log.debug, so a single malformed part (e.g. no matching MessageBodyReader, or an unexpected InputPart implementation) can't abort body/filename/content collection for the rest of the request.

MultipartContentDecoder.extractCharset rewrite

Three consecutive review comments against the original index-based charset= substring search (missing tab-as-OWS support, no whitespace tolerance around =, and no awareness of quoted parameter values) showed the search-for-a-literal-token approach itself was the problem, not any single missing character class. Replaced it with a proper media-type parameter tokenizer:

  • Skips the media type to the first ;/,, then walks name=value parameters, trimming RFC 7230 optional whitespace (space/tab) around both name and value.
  • Fully consumes quoted parameter values (with backslash-escape handling) as opaque tokens, so a charset-looking substring inside another parameter's quoted value (e.g. boundary="charset=oops") can never be mistaken for a real charset parameter.
  • Continues searching after an invalid charset value (Charset.forName throwing) instead of returning null immediately, in case a later, valid charset parameter exists.

This is a shared class used by ~17 other instrumentation modules (vertx-web, jersey2/3, tomcat7, jetty, netty41, undertow, akka-http, play25/26, commons-fileupload, ...); the public signature is unchanged and every behavioral delta is strictly more correct than the substring-search version.

Motivation

APPSEC-69139: extend the RESTEasy 3.0 multipart AppSec instrumentation to inspect multipart form-field values (server.request.body) with the same bounded-cap semantics already used for file content, replicating the pattern established by the Jersey 3 reference implementation.

The charset-parsing and filename-detection fixes were driven by Codex review comments on this PR that surfaced real charset-confusion and cap-bypass edge cases (tab-separated parameters, filename*, quoted charset=-looking substrings, and a null-Content-Type fallback that produced an unparseable string) as the implementation was iterated on.

Additional Notes

None.

Contributor Checklist

Jira ticket: APPSEC-69139

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.

…n handling

Cap collectBodyMap() by total accumulated values (not distinct field
names) to close a bypass where a repeated field name could exhaust the
limit. Filter to text/plain parts only and default undeclared charsets
to UTF-8 for body-map values, matching the Jersey reference. Widen
per-part read/header exceptions to Exception with debug logging so one
malformed part doesn't abort body/filename/content collection for the
whole request.
@jandro996 jandro996 added type: bug fix Bug fix inst: jax-rs JAX-RS instrumentation comp: asm waf Application Security Management (WAF) labels Jul 28, 2026
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

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

Reviewed commit: 6b93c566fe

ℹ️ 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-official

datadog-official Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🎯 Code Coverage (details)
Patch Coverage: 64.44%
Overall Coverage: 57.32% (-0.46%)

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

@dd-octo-sts

dd-octo-sts Bot commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

🟡 Java Benchmark SLOs — Performance SLO warning (near threshold)

Suite Status
Startup 🟡 warning

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
Scenario Candidate master Δ (95% CI of mean)
startup:insecure-bank:iast:Agent 13.96 s 13.92 s [-0.5%; +1.0%] (no difference)
startup:insecure-bank:tracing:Agent 13.00 s 13.02 s [-1.1%; +0.8%] (no difference)
startup:petclinic:appsec:Agent 16.85 s 16.56 s [+0.9%; +2.6%] (maybe worse)
startup:petclinic:iast:Agent 16.92 s 16.95 s [-1.2%; +0.8%] (no difference)
startup:petclinic:profiling:Agent 16.57 s 16.84 s [-2.6%; -0.8%] (maybe better)
startup:petclinic:sca:Agent 16.91 s 16.72 s [+0.3%; +2.0%] (maybe worse)
startup:petclinic:tracing:Agent 15.91 s 15.68 s [-2.8%; +5.7%] (no difference)

Commit: 9496d866 · 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.

@jandro996
jandro996 marked this pull request as ready for review July 28, 2026 07:57
@jandro996
jandro996 requested a review from a team as a code owner July 28, 2026 07:57
@jandro996
jandro996 requested review from ygree and removed request for a team July 28, 2026 07:57

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

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

The multipart changes preserve the intended bounded, text-only collection behavior across repeated fields, mixed file/text parts, missing headers, charset defaults, and body-reader failures. Full module tests could not run because the wrapper required an unavailable network download and the installed Gradle could not satisfy this checkout's Java 25 toolchain.

Was this helpful? React 👍 or 👎

📊 Validated against 1 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 6b93c56 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 6b93c566fe

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

A part declaring Content-Type: text/plain but also carrying a
filename attribute was still treated as a form field by
collectBodyMap(), so an attacker could pad the body-map cap with
disposable text/plain file parts and push a genuine field out of
server.request.body. Exclude any part with a filename attribute
(present, even empty) regardless of its declared media type, matching
the file/field distinction collectFilesContent() already uses.

Addresses a P1 finding from the Codex review on PR #12085.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 310e07ea2d

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

@jandro996
jandro996 marked this pull request as draft July 28, 2026 12:30
- hasFilenameParam() detects the extended filename* Content-Disposition
  parameter as well as plain filename, closing the same cap-padding
  bypass via the extended form (Codex P1 follow-up on PR #12085)
- add regression test for a part carrying only filename* (no plain
  filename)
…resteasy-bound-multipart' into alejandro.gonzalez/APPSEC-69139-resteasy-bound-multipart
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 6348c70d57

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

hasFilename() now excludes filename*-only parts from the body map, but
collectFilesContent() still gated on rawFilenameFromContentDisposition(),
which is filename*-blind — such a part reached neither AppSec callback.
Switch the gate to hasFilenameParam() so content inspection still covers
it (Codex P1 follow-up on PR #12085).
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: afbe420f61

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

…entDecoder

RFC 7230 optional whitespace (OWS) allows both space and horizontal tab.
extractCharset() only accepted space, so "type;\tcharset=X" was treated as
no-charset-declared, causing MultipartHelper to silently override the
client's declared charset with UTF-8.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 46aace9411

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

…harset param

extractCharset() only matched the literal token "charset=", so
"charset = X" or "charset =X" (whitespace around the equals sign)
was treated as no-charset-declared and silently overridden with UTF-8.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: fcb7cdcb2c

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

Comment thread internal-api/src/main/java/datadog/trace/api/http/MultipartContentDecoder.java Outdated
…nizer

Replace the index-based substring search with a proper media-type
parameter walk: quoted parameter values are consumed as opaque tokens
(so a "charset"-looking substring inside another parameter's quoted
value can never be mistaken for a real charset parameter), and an
invalid charset value no longer aborts the search: a later, valid
charset parameter is still found.
@jandro996
jandro996 force-pushed the alejandro.gonzalez/APPSEC-69139-resteasy-bound-multipart branch from 57589a2 to 3b634fc Compare July 29, 2026 11:25
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 21569459f1

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

contentTypeWithDefaultUtf8(null) produced a bare "charset=UTF-8" string
with no media type, which extractCharset()'s tokenizer cannot parse
(it assumes a media type always precedes the first parameter). This
made decodeBytes() fall back to the JVM platform charset instead of
UTF-8 for parts with no Content-Type header at all.
@jandro996

Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Another round soon, please!

Reviewed commit: 651db74d94

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

@jandro996
jandro996 marked this pull request as ready for review July 31, 2026 07:06
@jandro996
jandro996 requested review from a team as code owners July 31, 2026 07:06
@jandro996
jandro996 requested a review from claponcet July 31, 2026 07:06

@datadog-official datadog-official Bot left a comment

Copy link
Copy Markdown
Contributor

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

The multipart cap and classification changes are consistent with the Jersey reference and the added RESTEasy cases: text fields are bounded by total values, file parts do not consume that budget, and malformed part access is isolated. Charset parsing also passed adversarial declared, quoted, malformed, and substring cases; the full Gradle suites could not start because Java 25 is unavailable.

Was this helpful? React 👍 or 👎

📊 Validated against 8 scenarios · Open Bits AI session

🤖 Datadog Autotest · Commit 9496d86 · What is Autotest? · @DataDog review to ask questions · Any feedback? Reach out in #autotest

@chatgpt-codex-connector chatgpt-codex-connector Bot 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: 9496d866ed

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

Comment on lines +62 to +63
if (!isTextPlain(contentType) || hasFilename(headers)) {
continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep non-text form fields in an inspection path

When a filename-less multipart field declares a media type such as application/json, this condition drops it from server.request.body, while collectFilesContent() also skips it because it has no filename parameter. Previously every InputPart was added through getBodyAsString(), so applications using JSON/XML @RequestPart values now leave their payload absent from both AppSec callbacks, allowing attacks in those parts to evade inspection; retain all filename-less form fields or route non-text ones to another callback.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp: asm waf Application Security Management (WAF) inst: jax-rs JAX-RS instrumentation type: bug fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant