Skip to content

Add server.request.body.filenames and files_content for GlassFish/Payara#11267

Merged
gh-worker-dd-mergequeue-cf854d[bot] merged 26 commits into
masterfrom
alejandro.gonzalez/APPSEC-61873-payara
May 21, 2026
Merged

Add server.request.body.filenames and files_content for GlassFish/Payara#11267
gh-worker-dd-mergequeue-cf854d[bot] merged 26 commits into
masterfrom
alejandro.gonzalez/APPSEC-61873-payara

Conversation

@jandro996
Copy link
Copy Markdown
Member

@jandro996 jandro996 commented May 4, 2026

What Does This Do

  • New GlassFishMultipartInstrumentation@OnMethodExit advice on org.apache.catalina.fileupload.Multipart.getParts(). Reads the request field via @Advice.FieldValue typed as org.apache.catalina.connector.Request (the exact declared field type) and delegates to GlassFishBlockingHelper.
  • New GlassFishBlockingHelper — helper injected into the app classloader:
    • processPartsAndBlock(): iterates javax.servlet.http.Part objects via interface dispatch, collects filenames and file contents, fires requestFilesFilenames and requestFilesContent IG callbacks. Extracts HttpServletResponse for Payara via the org.apache.catalina.Request Catalina interface (see Additional Notes).
    • tryBlock(): commits blocking response via BlockResponseFunction or Servlet API fallback, then marks the trace segment as effectively blocked.
    • commitBlocking(): Servlet API fallback — writes the 403 response directly via HttpServletResponse for Payara, where BlockResponseFunction is never registered.

Build

  • New muzzle block glassfish for org.glassfish.main.extras:glassfish-embedded-all:[4.0, 6.1.0) with assertInverse = true. Upper bound excludes GlassFish 6.1.0+ (migrated to jakarta.* namespace).
  • compileOnly javax.servlet:javax.servlet-api:3.1.0 to access Part.getSubmittedFileName() (Servlet 3.1, not available in tomcat-catalina:7.0.4).
  • compileOnly org.glassfish.main.extras:glassfish-embedded-all:4.0 to access org.apache.catalina.Request and org.apache.catalina.Response Catalina interfaces, which Tomcat 7.x removed from its published API (see Additional Notes).

Tests

Unit tests for all methods in GlassFishBlockingHelperTest: form-field skip (null filename), empty filename, normal filename collection, content reading, max-files limit, getInputStream() failure fallback, sequential blocking (filenames blocks → content not fired), content blocking, non-Part object skip, effectivelyBlocked() throws → still returns true.

Motivation

GlassFish/Payara does not use Tomcat's Request.parseParts() — it delegates multipart parsing to org.apache.catalina.fileupload.Multipart.getParts(). Without this instrumentation, the server.request.body.filenames and server.request.body.files_content WAF addresses are never populated for GlassFish/Payara deployments.

Jira ticket: APPSEC-61873

Additional Notes

Why @Advice.FieldValue is typed as connector.Request and getResponse() is called through the org.apache.catalina.Request interface:

Multipart.request is declared as final org.apache.catalina.connector.Request request, so typing the advice parameter to match eliminates the Object cast. However, connector.Request.getResponse() cannot be called directly: in Tomcat the method returns org.apache.catalina.connector.Response (descriptor ()Lorg/apache/catalina/connector/Response;), while in GlassFish it returns org.apache.catalina.Response (descriptor ()Lorg/apache/catalina/Response;). The differing JVM descriptors would cause muzzle to reject the reference when checking against glassfish-embedded-all.

The solution is to call getResponse() through the org.apache.catalina.Request Catalina interface, whose getResponse() consistently returns org.apache.catalina.Response in both containers. connector.Request in GlassFish/Payara implements this interface (via HttpRequest), so the instanceof cast in GlassFishBlockingHelper is always true at runtime.

Why glassfish-embedded-all:4.0 is needed as compileOnly:

org.apache.catalina.Request and org.apache.catalina.Response are GlassFish-specific: Tomcat 7.x removed them from the published tomcat-catalina artifact. Adding glassfish-embedded-all:4.0 as compileOnly makes them available at compile time without affecting the produced JAR.

Why a Servlet API fallback for blocking:

TomcatServerInstrumentation instruments CoyoteAdapter.postParseRequest(), whose 4th argument is typed as org.apache.catalina.connector.Response. In Payara the equivalent type is PECoyoteResponse, so the method matcher never fires and BlockResponseFunction is never registered. GlassFishBlockingHelper.commitBlocking() writes the 403 response directly via HttpServletResponse as a fallback.

Muzzle range [4.0, 6.1.0):
GlassFish 6.1.0+ migrated to the jakarta.* namespace. This instrumentation references javax.servlet.http.Part, so it must be excluded from Jakarta-namespace versions.

Verified on Payara Micro 5.2022.1 via system-tests APPSEC_BLOCKING scenario with spring-boot-payara weblog: Test_Blocking_request_body_filenames and Test_Blocking_request_body_files_content both pass.

Contributor Checklist

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.

jandro996 added 10 commits May 4, 2026 13:57
…ntation

GlassFish 5 / Payara 5 does not have Request.parseParts() — instead
Request.getParts() delegates entirely to
org.apache.catalina.fileupload.Multipart.getParts(). That class uses its
own ArrayList<Part> field (INVOKEVIRTUAL, not INVOKEINTERFACE) and calls
a private initParts() instead of the Tomcat parseParts() that
ParsePartsInstrumentation looks for.

As a result, ParsePartsInstrumentation is a complete no-op on Payara:
the method matcher finds nothing, and the bytecode visitor intercepts no
Collection.add() calls. File names never reach the WAF even though the
request is parsed without error.

GlassFishMultipartInstrumentation targets
org.apache.catalina.fileupload.Multipart.getParts() — a public method
that exists only in GlassFish/Payara's web-core.jar and is therefore
automatically skipped by ByteBuddy on standard Tomcat. After the method
returns it iterates the Collection<Part> result and uses the existing
ParameterCollector reflection helpers (getSubmittedFileName()) to extract
file names, then fires requestFilesFilenames (and optionally
requestFilesContent) callbacks exactly as ParsePartsInstrumentation does.
…/Payara multipart instrumentation

- Add `muzzleDirective() { return "glassfish"; }` to prevent the instrumentation
  from being included in Tomcat muzzle tests, which would violate the `assertInverse`
  constraint of the `from703` block and fail CI
- Consolidate double `getCallbackProvider()` call into a single variable
- Add glassfish muzzle block to build.gradle for CI validation against
  glassfish-embedded-all artifacts
- Remove glassfish-embedded-all from testImplementation (its bundled Guava
  conflicts with the test bootstrap classpath setup)
…monsFileUpload pattern

Fetch both callbacks upfront before the collection loop, derive inspectContent
from the captured contentCb reference, and add early exit when neither callback
is registered. Eliminates a redundant getCallback() call and matches the pattern
used in CommonsFileUploadAppSecInstrumentation (PR #11137).
…sFish multipart instrumentation

On Java 11+ with GlassFish/Payara, reflective Method.invoke() from the injected
ParameterCollector helper (unnamed module) to PartItem (GlassFish named module) fails
with IllegalAccessException — setAccessible(true) is also blocked by the module system.

Replace reflection-based approach with a direct cast to javax.servlet.http.Part inside
the @Advice.OnMethodExit body. Because ByteBuddy inlines the advice into the target
class (Multipart), it runs in the same classloader/module context as PartItem, so
virtual dispatch through the Part interface works without any reflective access.

Changes:
- Rewrite GetPartsAdvice to cast each part to javax.servlet.http.Part and call
  getSubmittedFileName()/getInputStream()/getContentType() directly on the interface
- Add compileOnly javax.servlet:javax.servlet-api:3.1.0 so getSubmittedFileName()
  (added in Servlet 3.1) is available at compile time; tomcat-catalina:7.0.4 ships
  only Servlet 3.0
- Add setAccessible(true) to ParameterCollector.resolveAndCache() (defensive; used
  by ParsePartsInstrumentation on Tomcat where reflection still works)
- Remove helperClassNames() override — no helper injection needed for GlassFish path
…-item try/catch

An uncaught exception from getSubmittedFileName() on part N would short-circuit
the loop silently, leaving parts N+1..M unprocessed and the WAF with partial data.
suppress=Throwable.class on @Advice.OnMethodExit only swallows the exception after
the loop exits — it does not protect individual iterations.

Wrap the entire per-part body in try/catch(Exception ignored) so a broken PartItem
skips that part without affecting the remaining ones.
These calls were added during an attempt to fix the GlassFish IllegalAccessException
via reflection, but that approach was superseded by inlining the advice directly into
Multipart (which avoids reflection entirely). ParameterCollector is only used by
ParsePartsInstrumentation on standard Tomcat, where reflection on ApplicationPart
works without setAccessible(true) — as tests confirmed before this change.
Align with CommonsFileUploadAppSecInstrumentation: only populate the filenames
list when filenamesCallback is actually registered. Previously the list was
created and filled regardless, then silently dropped at dispatch time.
No correctness impact, but avoids unnecessary ArrayList allocation and string
copies when only the content callback is registered.
…strumentation

TomcatServerInstrumentation is muzzled out for Payara's response type, so
BlockResponseFunction is never registered. Add GlassFishBlockingHelper that
commits the blocking response directly via Servlet API, extracting the
HttpServletResponse from Multipart.request (private field) via reflection.
The setAccessible call works because the advice is inlined into Multipart.getParts()
— the same module as the field owner.

Verified against system-tests APPSEC_BLOCKING with spring-boot-payara:
- Test_Blocking_request_body_filenames: passes (was missing_feature)
- Test_Blocking_request_body_files_content: passes (was missing_feature)
…rumentation

- Use try-with-resources for OutputStream in GlassFishBlockingHelper
- Cache Config limit values as static finals in GlassFishBlockingHelper
  to avoid per-request Config.get() calls (consistent with ParameterCollector)
- Remove Config import from GlassFishMultipartInstrumentation (now reads
  limits from GlassFishBlockingHelper static fields)
- Add comment to skipVersions explaining the jakarta namespace reason
- Add assertInverse to glassfish muzzle block for defensive CI coverage
@jandro996 jandro996 changed the title Add AppSec multipart filenames and file content detection for GlassFish/Payara Add server.request.body.filenames and files_content AppSec addresses for GlassFish/Payara May 6, 2026
jandro996 added 6 commits May 6, 2026 16:47
…ckingException for Payara blocking

ByteBuddy inlines advice into Multipart.getParts() (package org.apache.catalina.fileupload),
which is a different package than GlassFishBlockingHelper. Package-private static fields
caused IllegalAccessError at runtime crashing the Payara container.

BlockingException thrown from getParts() propagated through Payara's Grizzly pipeline and
caused an ungraceful shutdown. Replace with parts = Collections.emptyList() so the response
is committed (via commitBlocking fallback) and the controller skips processing the parts.
…assFishBlockingHelper

Extract the repeated brf/fallback blocking pattern from the GlassFish multipart advice
into GlassFishBlockingHelper.tryBlock(). This eliminates the duplication between the
filenames and content blocking paths, and fixes the ordering issue where blocked=true
was assigned after effectivelyBlocked() — if effectivelyBlocked() threw, blocked would
stay false and the content callback would fire after filenames already blocked.

tryBlock() uses a two-phase try/catch: the outer block handles commit failures (returning
false), while the inner block wraps effectivelyBlocked() so a thrown exception does not
suppress the true return value when the response was already committed.

Add GlassFishBlockingHelperTest covering all branches of commitBlocking() and tryBlock().
…elper

Extract the part-iteration loop and IG callback dispatch from the advice
into a new GlassFishBlockingHelper.processPartsAndBlock() static method.
The advice now contains only the inlined reflection block (which must stay
inlined due to Java module-system constraints) and a single call to the
helper.

Unit tests added for processPartsAndBlock(): form-field skip, empty
filename, normal filename, content reading, maxFiles limit, getInputStream
failure fallback, sequential callback ordering (filenames blocks → content
not fired), content callback blocks → returns true, non-Part object skip.
@jandro996
Copy link
Copy Markdown
Member Author

@codex review

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Already looking forward to the next diff.

ℹ️ 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 changed the title Add server.request.body.filenames and files_content AppSec addresses for GlassFish/Payara Add server.request.body.filenames and files_content for GlassFish/Payara May 8, 2026
@jandro996 jandro996 added type: enhancement Enhancements and improvements comp: asm waf Application Security Management (WAF) labels May 8, 2026
@jandro996 jandro996 marked this pull request as ready for review May 11, 2026 09:48
@jandro996 jandro996 requested a review from a team as a code owner May 11, 2026 09:48
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

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: f6f2feb51c

ℹ️ 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 added 7 commits May 15, 2026 15:18
…ation

Use @advice.FieldValue("request") instead of getDeclaredField+setAccessible
to access Multipart.request — ByteBuddy inlines direct field access, no
reflection needed for the field itself. getResponse() still uses reflection
since the return type is a Payara-internal class.

Add explanatory comments to all catch(Exception ignored) blocks in both
the advice and GlassFishBlockingHelper.
Typing the field to org.apache.catalina.connector.Request allows calling
getResponse() directly, removing the Method reflection block entirely.
org.apache.catalina.connector.Request is not exported by glassfish-embedded-all,
so typing the field parameter to that class adds a bytecode reference that muzzle
cannot resolve, causing the AssertPass check for GlassFish 4.0 to fail.

Keep @advice.FieldValue (avoids setAccessible across module boundaries) but type
as Object. getResponse() stays reflective for the same reason.
Type @advice.FieldValue("request") as org.apache.catalina.connector.Request
instead of Object. Pass catRequest directly to GlassFishBlockingHelper, which
casts it to the org.apache.catalina.Request Catalina interface (available in
GlassFish/Payara via HttpRequest) to reach HttpServletResponse without any
Method.invoke() reflection.

The org.apache.catalina.Request.getResponse() method returns
org.apache.catalina.Response with a stable JVM descriptor shared between Tomcat
and GlassFish, so no muzzle descriptor mismatch occurs. This is not possible
calling connector.Request.getResponse() directly because Tomcat returns
connector.Response while GlassFish returns org.apache.catalina.Response.

Add glassfish-embedded-all:4.0 as compileOnly dependency so that
org.apache.catalina.Request and org.apache.catalina.Response — GlassFish-specific
interfaces removed from Tomcat 7.x public API — are available at compile time.

Also add missing comment to the empty catch for effectivelyBlocked().
jandro996 added 2 commits May 20, 2026 15:42
Using the Catalina Request interface (instead of connector.Request) as
the @advice.FieldValue parameter type lets muzzle verify that the
instrumented field type (connector.Request) still implements that interface
in every supported GlassFish version, catching any future breakage at
build time.

The interface's getResponse() descriptor is stable across Tomcat and
GlassFish, which also removes the need for the instanceof cast in the
helper. Add glassfish-embedded-all:4.0 as testCompileOnly so the test
sources can resolve the interface type.
@datadog-prod-us1-3

This comment has been minimized.

Copy link
Copy Markdown
Member

@manuel-alvarez-alvarez manuel-alvarez-alvarez left a comment

Choose a reason for hiding this comment

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

LGTM

@jandro996 jandro996 added this pull request to the merge queue May 21, 2026
@dd-octo-sts
Copy link
Copy Markdown
Contributor

dd-octo-sts Bot commented May 21, 2026

/merge

@gh-worker-devflow-routing-ef8351
Copy link
Copy Markdown

gh-worker-devflow-routing-ef8351 Bot commented May 21, 2026

View all feedbacks in Devflow UI.

2026-05-21 09:54:18 UTC ℹ️ Start processing command /merge


2026-05-21 09:54:23 UTC ℹ️ MergeQueue: pull request added to the queue

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


2026-05-21 11:54:46 UTCMergeQueue: The build pipeline has timeout

The merge request has been interrupted because the build 8649452320257527611 took longer than expected. The current limit for the base branch 'master' is 120 minutes.

@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 21, 2026
@jandro996 jandro996 added this pull request to the merge queue May 21, 2026
@dd-octo-sts
Copy link
Copy Markdown
Contributor

dd-octo-sts Bot commented May 21, 2026

/merge

@gh-worker-devflow-routing-ef8351
Copy link
Copy Markdown

gh-worker-devflow-routing-ef8351 Bot commented May 21, 2026

View all feedbacks in Devflow UI.

2026-05-21 12:54:53 UTC ℹ️ Start processing command /merge


2026-05-21 12:54:59 UTC ℹ️ MergeQueue: pull request added to the queue

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


2026-05-21 14:04:51 UTC ℹ️ MergeQueue: This merge request was merged

@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 21, 2026
@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot merged commit 5f550ab into master May 21, 2026
571 checks passed
@gh-worker-dd-mergequeue-cf854d gh-worker-dd-mergequeue-cf854d Bot deleted the alejandro.gonzalez/APPSEC-61873-payara branch May 21, 2026 14:04
@github-actions github-actions Bot added this to the 1.63.0 milestone May 21, 2026
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) type: enhancement Enhancements and improvements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants