[
https://issues.apache.org/jira/browse/WW-5474?focusedWorklogId=1031718&page=com.atlassian.jira.plugin.system.issuetabpanels:worklog-tabpanel#worklog-1031718
]
ASF GitHub Bot logged work on WW-5474:
--------------------------------------
Author: ASF GitHub Bot
Created on: 22/Jul/26 17:00
Start Date: 22/Jul/26 17:00
Worklog Time Spent: 10m
Work Description: lukaszlenart commented on code in PR #1806:
URL: https://github.com/apache/struts/pull/1806#discussion_r3632192101
##########
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:
##########
@@ -115,18 +115,29 @@ protected void processUpload(HttpServletRequest request,
String saveDir) throws
RequestContext requestContext = createRequestContext(request);
- for (DiskFileItem item :
servletFileUpload.parseRequest(requestContext)) {
- // Track all DiskFileItem instances for cleanup - this is critical
for security
- // as it ensures temporary files are properly cleaned up even if
processing fails
- diskFileItems.add(item);
-
+ int fileCount = 0;
+ int parameterCount = 0;
+ // parseRequest() fully materializes every part (spilling large ones
to disk) before we
+ // iterate, so register them all for cleanup up front - otherwise a
fail-closed breach
+ // mid-loop would leak the temp files of every part after the
breaching one.
+ List<DiskFileItem> items =
servletFileUpload.parseRequest(requestContext);
+ diskFileItems.addAll(items);
+ for (DiskFileItem item : items) {
LOG.debug(() -> "Processing a form field: " +
normalizeSpace(item.getFieldName()));
if (item.isFormField()) {
// Process regular form fields (text inputs, checkboxes, etc.)
+ if (item.getFieldName() != null) {
+ enforceMaxParameterCount(parameterCount,
item.getFieldName());
+ parameterCount++;
+ }
processNormalFormField(item, charset);
} else {
- // Process file upload fields
+ // Process file upload fields (only count parts that carry an
actual file)
LOG.debug(() -> "Processing a file: " +
normalizeSpace(item.getFieldName()));
+ if (item.getName() != null &&
!item.getName().trim().isEmpty()) {
+ enforceMaxFiles(fileCount, item.getName());
+ fileCount++;
+ }
Review Comment:
Addressed in c566aa42d. Worth noting: commons-fileupload2 `parseRequest`
silently drops parts that have no `name` attribute before we ever iterate, so
this divergence is not actually reachable via the `jakarta` path. I have
nonetheless added `&& item.getFieldName() != null` to the count/enforce guard
so the accept-criteria matches `JakartaStreamMultiPartRequest`. Thanks!
##########
core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java:
##########
@@ -226,9 +239,10 @@ protected JakartaServletDiskFileUpload
prepareServletFileUpload(Charset charset,
LOG.debug("Applies max size: {} to file upload request", maxSize);
servletFileUpload.setMaxSize(maxSize);
}
- if (maxFiles != null) {
- LOG.debug("Applies max files number: {} to file upload request",
maxFiles);
- servletFileUpload.setMaxFileCount(maxFiles);
+ if (maxFiles != null && maxFiles >= 0 && maxParameterCount != null &&
maxParameterCount >= 0) {
+ long maxParts = maxFiles + maxParameterCount;
+ LOG.debug("Applies total parts backstop: {} to file upload
request", maxParts);
+ servletFileUpload.setMaxFileCount(maxParts);
}
Review Comment:
Fixed in c566aa42d — the total-parts backstop now uses
`Math.addExact(maxFiles, maxParameterCount)` and clamps to `Long.MAX_VALUE` on
overflow, so it can no longer wrap to a negative value and silently disable the
commons file-count backstop. Thanks!
##########
core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java:
##########
@@ -422,6 +422,74 @@ public void errorDuplicationPrevention() throws
IOException {
assertThat(multiPartRequest.getErrors()).hasSize(1);
}
+ @Test
+ public void manyFormFieldsWithFewFilesAreAccepted() throws IOException {
+ // Regression for WW-5474: maxFiles must not count form fields.
+ StringBuilder content = new StringBuilder();
+ for (int i = 0; i < 10; i++) {
+ content.append(formField("field" + i, "value" + i));
+ }
+ content.append(formFile("file1", "test1.csv", "1,2,3,4"));
+ content.append(formFile("file2", "test2.csv", "5,6,7,8"));
+ content.append(endline).append("--").append(boundary).append("--");
+
mockRequest.setContent(content.toString().getBytes(StandardCharsets.UTF_8));
+
+ multiPart.setMaxFiles("2"); // only 2 files, but 10 fields present
+ multiPart.parse(mockRequest, tempDir);
+
+ assertThat(multiPart.getErrors()).isEmpty();
+ assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
+
.asInstanceOf(InstanceOfAssertFactories.LIST).containsOnly("file1", "file2");
Review Comment:
False positive here: AssertJ `IteratorAssert.toIterable()` drains the
iterator into a `List`, so `asInstanceOf(InstanceOfAssertFactories.LIST)`
succeeds — no `ClassCastException` occurs (all tests pass). This idiom is
already used in several existing `AbstractMultiPartRequestTest` assertions, so
I am keeping it consistent with the surrounding tests.
##########
core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java:
##########
@@ -216,14 +216,63 @@ public void exceedsMaxFilesPath() throws IOException {
// when - set max files to 1
multiPart.setMaxFiles("1");
multiPart.parse(mockRequest, tempDir);
-
- // then - should have only 1 file and errors for others
- assertThat(multiPart.uploadedFiles).hasSize(1);
+
+ // then - fail-closed: no partial files, one error
+ assertThat(multiPart.uploadedFiles).isEmpty();
assertThat(multiPart.getErrors())
- .isNotEmpty()
- .allSatisfy(error ->
-
assertThat(error.getTextKey()).isEqualTo("struts.messages.upload.error.FileUploadFileCountLimitException")
- );
+ .map(LocalizedMessage::getTextKey)
+
.containsExactly("struts.messages.upload.error.FileUploadFileCountLimitException");
+ }
+
+ @Test
+ public void streamManyFormFieldsWithFewFilesAreAccepted() throws
IOException {
+ StringBuilder content = new StringBuilder();
+ for (int i = 0; i < 10; i++) {
+ content.append(formField("field" + i, "value" + i));
+ }
+ content.append(formFile("file1", "test1.csv", "1,2,3,4"));
+ content.append(endline).append("--").append(boundary).append("--");
+
mockRequest.setContent(content.toString().getBytes(StandardCharsets.UTF_8));
+
+ multiPart.setMaxFiles("1");
+ multiPart.parse(mockRequest, tempDir);
+
+ assertThat(multiPart.getErrors()).isEmpty();
+ assertThat(multiPart.getFileParameterNames().asIterator()).toIterable()
+
.asInstanceOf(InstanceOfAssertFactories.LIST).containsOnly("file1");
Review Comment:
False positive here: AssertJ `IteratorAssert.toIterable()` drains the
iterator into a `List`, so `asInstanceOf(InstanceOfAssertFactories.LIST)`
succeeds — no `ClassCastException` occurs (all tests pass). This idiom is
already used in several existing `AbstractMultiPartRequestTest` assertions, so
I am keeping it consistent with the surrounding tests.
##########
core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:
##########
@@ -115,18 +115,31 @@ protected void processUpload(HttpServletRequest request,
String saveDir) throws
RequestContext requestContext = createRequestContext(request);
- for (DiskFileItem item :
servletFileUpload.parseRequest(requestContext)) {
- // Track all DiskFileItem instances for cleanup - this is critical
for security
- // as it ensures temporary files are properly cleaned up even if
processing fails
- diskFileItems.add(item);
-
+ int fileCount = 0;
+ int parameterCount = 0;
+ // parseRequest() fully materializes every part (spilling large ones
to disk) before we
+ // iterate, so register them all for cleanup up front - otherwise a
fail-closed breach
+ // mid-loop would leak the temp files of every part after the
breaching one.
+ List<DiskFileItem> items =
servletFileUpload.parseRequest(requestContext);
+ diskFileItems.addAll(items);
+ for (DiskFileItem item : items) {
LOG.debug(() -> "Processing a form field: " +
normalizeSpace(item.getFieldName()));
Review Comment:
Good catch — fixed in 794c29db4 by moving the `"Processing a form field"`
debug line into the `isFormField` branch; the file branch already logs
`"Processing a file"`, so parts are no longer mislabelled.
Issue Time Tracking
-------------------
Worklog Id: (was: 1031718)
Time Spent: 1h 10m (was: 1h)
> struts.multipart.maxFiles does not work as described/expected
> -------------------------------------------------------------
>
> Key: WW-5474
> URL: https://issues.apache.org/jira/browse/WW-5474
> Project: Struts 2
> Issue Type: Bug
> Affects Versions: 6.3.0
> Reporter: nikos dimitrakas
> Assignee: Lukasz Lenart
> Priority: Major
> Fix For: 7.3.0
>
> Time Spent: 1h 10m
> Remaining Estimate: 0h
>
> According to the documentation the property struts.multipart.maxFiles (which
> defaults to 256) is supposed to set a limit to how many files a multi-part
> form submission may include. But in reality, the specified value sets a limit
> to how many parameter values the form may submit, including strings,
> integers, booleans (from all input fields, checkboxes, hiddens, etc).
> The problem is in the method JakartaMultiPartRequest.parsePrequest (part of
> struts) when in the last line it calls upload.parseRequest which is in
> FileUploadBase (part of commons-fileupload). That methods tries to find the
> FileItems from the provided RequestContext, but considers every parameter
> value to be a FileItem and then throws a new FileCountLimitExceededException
> when the number of parameter values (of any type) reaches the fileCountMax.
> I am not sure if the problem is in struts or in fileupload. Maybe the
> provided RequestContext should somehow only include the file parameters so
> that only they get counted. Or perhaps the documentation should be changed to
> clarify that struts.multipart.maxFiles specifies the maximum number of
> parameter values a multipart form may include.
> I also found the issue
> [FILEUPLOAD-351|https://issues.apache.org/jira/browse/FILEUPLOAD-351] that
> seems to point out the confusing behaviour. But until the issue has been
> resolved in fileupload, struts behaves contrary to its own documentation at
> [https://struts.apache.org/core-developers/file-upload]
>
--
This message was sent by Atlassian Jira
(v8.20.10#820010)