Skip to content

Add AppSec multipart filenames and file content detection for GlassFish/Payara#11267

Draft
jandro996 wants to merge 8 commits intomasterfrom
alejandro.gonzalez/APPSEC-61873-payara
Draft

Add AppSec multipart filenames and file content detection for GlassFish/Payara#11267
jandro996 wants to merge 8 commits intomasterfrom
alejandro.gonzalez/APPSEC-61873-payara

Conversation

@jandro996
Copy link
Copy Markdown
Member

@jandro996 jandro996 commented May 4, 2026

What Does This Do

Instruments org.apache.catalina.fileupload.Multipart.getParts() — a GlassFish/Payara-specific class that does not exist in standard Tomcat — to report uploaded file names and file contents to the AppSec WAF via the server.request.body.filenames and server.request.body.files_content IG events.

Key design choice — direct javax.servlet.http.Part cast instead of reflection:

On Java 11+ with GlassFish/Payara, reflective Method.invoke() from an injected ByteBuddy helper (unnamed module) to PartItem (GlassFish named module) fails with IllegalAccessException — and setAccessible(true) is also blocked by the module system. The fix is to cast each part object directly to javax.servlet.http.Part inside the @Advice.OnMethodExit body. Because ByteBuddy inlines the advice into the target class (org.apache.catalina.fileupload.Multipart), the advice body runs in the same classloader/module context as PartItem, so virtual dispatch through the Part interface works without any reflective access. No helperClassNames() override is needed.

Changes:

  • GlassFishMultipartInstrumentation: new InstrumenterModule.AppSec for org.apache.catalina.fileupload.Multipart
    • Hooks getParts() on exit; casts each element to javax.servlet.http.Part
    • Calls getSubmittedFileName() to distinguish file uploads from form fields (null → skip, empty string → file without name)
    • Fires requestFilesFilenames callback if any filenames are found
    • Fires requestFilesContent callback (guarded by contents.size() < maxFiles) using MultipartContentDecoder.readInputStream
    • Blocking action handling for both callbacks with effectivelyBlocked()
    • muzzleDirective() returns "glassfish" to exclude from the from703 assertInverse=true Tomcat muzzle check
  • build.gradle (tomcat-appsec-7.0):
    • Added glassfish muzzle pass block (org.glassfish.main.extras:glassfish-embedded-all:[4.0,])
    • Added compileOnly javax.servlet:javax.servlet-api:3.1.0Part.getSubmittedFileName() was introduced in Servlet 3.1 (GlassFish 4+ = Java EE 7); tomcat-catalina:7.0.4 ships only Servlet 3.0
  • ParameterCollector: added setAccessible(true) after each getMethod() call in resolveAndCache() (defensive; used by ParsePartsInstrumentation on Tomcat where reflection is still the correct approach)

Motivation

GlassFish and Payara (GlassFish-based) use a different multipart parsing path than standard Tomcat: Request.getParts() delegates to org.apache.catalina.fileupload.Multipart.getParts() instead of Request.parseParts(). The existing Tomcat instrumentation (ParsePartsInstrumentation) hooks parseParts() and is therefore never triggered on GlassFish/Payara, leaving file upload events unreported to the WAF.

Jira ticket: APPSEC-61873

Additional Notes

Why inlining beats helper injection for GlassFish:

The Java Platform Module System (JPMS) on Java 11+ enforces strong encapsulation for named modules. GlassFish packages org.apache.catalina.fileupload.PartItem in a named module; ByteBuddy-injected helpers run in the unnamed module. Method.setAccessible(true) does not cross this boundary, so reflective invocation silently fails (caught by catch (Exception ignored) in the helper) and no data reaches the WAF. The advice inlining mechanism sidesteps this entirely — the inlined bytecode is part of the Multipart class itself, in the same module as PartItem, so all method calls through the javax.servlet.http.Part interface resolve normally via virtual dispatch.

Contributor Checklist

Jira ticket: APPSEC-61873

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 8 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant