WW-5474 Count files only for maxFiles, add maxParameterCount#1806
WW-5474 Count files only for maxFiles, add maxParameterCount#1806lukaszlenart wants to merge 9 commits into
Conversation
…rCount Spec for correcting struts.multipart.maxFiles to count file parts only (consistently across the jakarta and jakarta-stream parsers) and adding struts.multipart.maxParameterCount to cap non-file form fields, restoring the DoS guard the old accidental total-part cap provided. Fail-closed on breach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…rCount Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erCount (jakarta) The jakarta parser passed maxFiles to commons-fileupload2 setMaxFileCount, which counts every part (fields + files), so maxFiles wrongly limited total parameters. Enforce a files-only count and non-file field count in Struts, failing closed on breach; keep a total-parts commons backstop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ckstop prepareServletFileUpload applied the total-parts backstop whenever both maxFiles and maxParameterCount were non-null, without checking for the -1 "unlimited" sentinel already honored by enforceMaxFiles/enforceMaxParameterCount. With maxFiles=-1 and maxParameterCount=256, maxParts computed to 255 and was passed to commons-fileupload2's setMaxFileCount (which counts ALL parts), wrongly rejecting large file-only uploads. Only apply the backstop when both limits are finite (non-null and >= 0). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… to stream parser Replace the field-name-based exceedsMaxFiles with the shared files-only enforcement and add parameter-count enforcement, matching the jakarta parser and failing closed on breach. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…k on fail-closed breach servletFileUpload.parseRequest() fully materializes every part - spilling large ones to disk - before processUpload() iterates over the result. The loop only added each DiskFileItem to diskFileItems as it was reached, so when enforceMaxFiles/enforceMaxParameterCount threw mid-loop on a breach, every item positioned after the breaching one was never registered for cleanup. With no FileCleaningTracker on the factory, cleanUp() had no way to reclaim those temp files, leaking disk space on the hardening path. Materialize the parsed list once and register all items for cleanup before processing so cleanUp() reclaims every temp file regardless of where enforcement aborts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… S2629) Wrap the LOG.debug calls in enforceMaxFiles/enforceMaxParameterCount with isDebugEnabled() so normalizeSpace() is not evaluated when debug is disabled, matching the exceedsMaxStringLength pattern in the same class. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR fixes long-standing inconsistencies in multipart upload limiting for the Jakarta multipart parsers by making struts.multipart.maxFiles count file parts only, adding a new non-file field cap (struts.multipart.maxParameterCount), and enforcing fail-closed behavior with updated message wiring and tests.
Changes:
- Align
maxFilessemantics acrossjakartaandjakarta-streamparsers to count only actual uploaded files, not form fields. - Introduce
struts.multipart.maxParameterCount(default256) to cap non-file form fields and preserve DoS protection. - Harden failure handling to clear collected parameters/files on upload-limit exceptions, and expand unit test coverage for the new semantics.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| docs/superpowers/specs/2026-07-22-WW-5474-multipart-maxfiles-semantics-design.md | New design spec documenting the corrected semantics and fail-closed behavior. |
| docs/superpowers/plans/2026-07-22-WW-5474-multipart-maxfiles-semantics.md | New implementation plan describing tasks, wiring, and test expectations. |
| core/src/main/java/org/apache/struts2/StrutsConstants.java | Adds the new STRUTS_MULTIPART_MAX_PARAMETER_COUNT constant. |
| core/src/main/resources/org/apache/struts2/default.properties | Updates maxFiles comment and adds default maxParameterCount=256. |
| core/src/main/resources/org/apache/struts2/struts-messages.properties | Adds localized message key for FileUploadParameterCountLimitException. |
| core/src/main/java/org/apache/struts2/dispatcher/multipart/FileUploadParameterCountLimitException.java | New exception type for parameter-count limit breaches. |
| core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java | Adds maxParameterCount injection, shared enforcement helpers, commons backstop logic, and fail-closed clearing. |
| core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java | Reworks counting to separate file vs parameter parts and ensures disk items are registered for cleanup up-front. |
| core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java | Removes field-name-based counting and replaces it with true per-part counters plus shared enforcement calls. |
| core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java | Adds regression and limit tests, plus cleanup/backstop coverage. |
| core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java | Updates fail-closed expectations and adds tests for the new counting rules and parameter cap. |
| core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java | Adds a unit test verifying the new setter stores the configured value. |
Comments suppressed due to low confidence (1)
core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java:387
clearCollectedData()is called forFileUploadException, but not for the genericIOExceptionpath. If anIOExceptionoccurs after some fields/files were already collected (e.g., read error mid-request), the action could still see partial data even though an upload error was recorded. If the intent is broadly fail-closed on parse aborts, clear collected data in theIOExceptioncatch as well.
} catch (IOException e) {
LOG.warn("Unable to parse request", e);
LocalizedMessage errorMessage = buildErrorMessage(e.getClass(), e.getMessage(), new Object[]{});
addErrorIfAbsent(errorMessage);
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…flow guard - JakartaMultiPartRequest: only count/enforce a file part toward maxFiles when it has a non-null field name, matching JakartaStreamMultiPartRequest's accept criteria (defensive: commons-fileupload2 already drops parts without a name attribute before parseRequest returns, so the two parsers stay consistent regardless). - AbstractMultiPartRequest: compute the total-parts backstop with Math.addExact and clamp to Long.MAX_VALUE on overflow, so extremely large configured limits cannot wrap negative and silently disable the commons backstop. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ields Move the debug log into the isFormField branch so file parts are not mislabelled; the file branch already logs "Processing a file". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|



Fixes WW-5474
Problem
struts.multipart.maxFiles(default256) is documented as a cap on the number of uploaded files, but it did not behave that way, and the two Jakarta parsers disagreed:jakartaparser (default) passed the value to commons-fileupload2setMaxFileCount, whoseparseRequestcounts every part — form fields and files. SomaxFilesactually limited the total number of parameters, firing spuriously on forms with many normal fields and few (or zero) files. This is the reported bug.jakarta-streamparser never received commons enforcement (its streaming iterator ignoresmaxFileCount) and counted distinct field names via its ownexceedsMaxFiles, so multiple files under one field name collapsed to one.Change
struts.multipart.maxFilesnow counts file parts only, identically in both parsers.struts.multipart.maxParameterCount(default256) caps non-file form fields, restoring the DoS guard the old accidental total-part cap incidentally provided. The two limits are orthogonal.-1) or unset limit means unlimited, following the commons-fileupload2 convention.jakartaparser keeps a coarse total-parts backstop (maxFiles + maxParameterCount, only when both are finite) so gross floods abort early inside commons.Design:
docs/superpowers/specs/2026-07-22-WW-5474-multipart-maxfiles-semantics-design.mdBehavior changes to be aware of (please review)
jakarta-streamgains a parameter-count cap it never had. Ajakarta-streamapp legitimately posting more than 256 form fields will now fail at field 257 unless it raisesstruts.multipart.maxParameterCount. The defaultjakartaparser is strictly more permissive than before (previously fields+files shared one 256 budget; now 256 each).maxSize/maxFileSizebreaches too (not only the new limits). Previously the stream parser could expose parts collected before a size breach; now a rejected request exposes nothing. This is intentional hardening.512total on thejakartaparser surfaces the genericFileUploadFileCountLimitExceptionvia the commons backstop, while the stream parser reports the preciseFileUploadParameterCountLimitException. Both fail closed.Tests
New/updated unit tests in
JakartaMultiPartRequestTest,JakartaStreamMultiPartRequestTest,AbstractMultiPartRequestTestcover: many fields + few files accepted (the reported regression), over-maxFilesand over-maxParameterCountfail-closed, multiple files under one field name counted individually, andmaxFiles=-1not clamped by the backstop. Fullcoremodule suite green (3003 tests);ActionFileUploadInterceptorTestregression green.🤖 Generated with Claude Code