This is an automated email from the ASF dual-hosted git repository.

kusal pushed a commit to branch WW-5528-multipart-illegal-char-errors
in repository https://gitbox.apache.org/repos/asf/struts.git

commit 96ee51fb1297d84f02a73c488c49c6bded9987db
Author: Kusal Kithul-Godage <g...@kusal.io>
AuthorDate: Thu Feb 6 11:39:09 2025 +1100

    WW-5528 Ensure multipart upload illegal characters reported as error
---
 .../multipart/AbstractMultiPartRequest.java        | 62 +++++++++++++++-------
 .../multipart/JakartaMultiPartRequest.java         | 28 +++-------
 .../multipart/JakartaStreamMultiPartRequest.java   | 22 +++-----
 .../org/apache/struts2/struts-messages.properties  |  4 ++
 .../multipart/AbstractMultiPartRequestTest.java    |  8 +--
 5 files changed, 66 insertions(+), 58 deletions(-)

diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java
index 7f848a56d..00e774a72 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java
@@ -18,14 +18,13 @@
  */
 package org.apache.struts2.dispatcher.multipart;
 
-import jakarta.servlet.http.HttpServletRequest;
 import org.apache.commons.fileupload2.core.FileUploadByteCountLimitException;
 import org.apache.commons.fileupload2.core.FileUploadContentTypeException;
 import org.apache.commons.fileupload2.core.FileUploadException;
 import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
 import org.apache.commons.fileupload2.core.FileUploadSizeException;
 import 
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
-import org.apache.commons.lang3.BooleanUtils;
+import org.apache.commons.io.FilenameUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
@@ -35,6 +34,7 @@ import org.apache.struts2.inject.Inject;
 import org.apache.struts2.security.DefaultExcludedPatternsChecker;
 import org.apache.struts2.security.ExcludedPatternsChecker;
 
+import jakarta.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.file.Path;
@@ -45,6 +45,8 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
+import static org.apache.commons.lang3.StringUtils.normalizeSpace;
+
 /**
  * Abstract class with some helper methods, it should be used
  * when starting development of another implementation of {@link 
MultiPartRequest}
@@ -52,6 +54,8 @@ import java.util.Map;
 public abstract class AbstractMultiPartRequest implements MultiPartRequest {
 
     protected static final String 
STRUTS_MESSAGES_UPLOAD_ERROR_PARAMETER_TOO_LONG_KEY = 
"struts.messages.upload.error.parameter.too.long";
+    protected static final String 
STRUTS_MESSAGES_UPLOAD_ERROR_ILLEGAL_CHARACTERS_FIELD = 
"struts.messages.upload.error.illegal.characters.field";
+    protected static final String 
STRUTS_MESSAGES_UPLOAD_ERROR_ILLEGAL_CHARACTERS_NAME = 
"struts.messages.upload.error.illegal.characters.name";
 
     private static final Logger LOG = 
LogManager.getLogger(AbstractMultiPartRequest.class);
 
@@ -116,13 +120,14 @@ public abstract class AbstractMultiPartRequest implements 
MultiPartRequest {
 
     private final ExcludedPatternsChecker patternsChecker;
 
-    protected AbstractMultiPartRequest(String dmiValue) {
-        patternsChecker = new DefaultExcludedPatternsChecker();
-        if (BooleanUtils.toBoolean(dmiValue)) {
-            ((DefaultExcludedPatternsChecker) 
patternsChecker).setAdditionalExcludePatterns(EXCLUDED_FILE_PATTERN_WITH_DMI_SUPPORT);
-        } else {
-            ((DefaultExcludedPatternsChecker) 
patternsChecker).setAdditionalExcludePatterns(EXCLUDED_FILE_PATTERN);
-        }
+    protected AbstractMultiPartRequest() {
+        this(false);
+    }
+
+    protected AbstractMultiPartRequest(boolean dmiValue) {
+        var patternsChecker = new DefaultExcludedPatternsChecker();
+        patternsChecker.setAdditionalExcludePatterns(dmiValue ? 
EXCLUDED_FILE_PATTERN_WITH_DMI_SUPPORT : EXCLUDED_FILE_PATTERN);
+        this.patternsChecker = patternsChecker;
     }
 
     /**
@@ -302,16 +307,7 @@ public abstract class AbstractMultiPartRequest implements 
MultiPartRequest {
      * @return the canonical name based on the supplied filename
      */
     protected String getCanonicalName(final String originalFileName) {
-        String fileName = originalFileName;
-
-        int forwardSlash = fileName.lastIndexOf('/');
-        int backwardSlash = fileName.lastIndexOf('\\');
-        if (forwardSlash != -1 && forwardSlash > backwardSlash) {
-            fileName = fileName.substring(forwardSlash + 1);
-        } else {
-            fileName = fileName.substring(backwardSlash + 1);
-        }
-        return fileName;
+        return FilenameUtils.getName(originalFileName);
     }
 
     /**
@@ -443,4 +439,32 @@ public abstract class AbstractMultiPartRequest implements 
MultiPartRequest {
         return patternsChecker.isExcluded(fileName).isExcluded();
     }
 
+    protected boolean isInvalidInput(String fieldName, String fileName) {
+        // Skip file uploads that don't have a file name - meaning that no 
file was selected.
+        if (fileName == null || fileName.isBlank()) {
+            LOG.debug(() -> "No file has been uploaded for the field: " + 
normalizeSpace(fieldName));
+            return true;
+        }
+
+        if (isExcluded(fileName)) {
+            var normalizedFileName = normalizeSpace(fileName);
+            LOG.debug("File name [{}] is not accepted", normalizedFileName);
+            errors.add(new LocalizedMessage(getClass(), 
STRUTS_MESSAGES_UPLOAD_ERROR_ILLEGAL_CHARACTERS_NAME, null,
+                    new String[]{normalizedFileName}));
+            return true;
+        }
+
+        return isInvalidInput(fieldName);
+    }
+
+    protected boolean isInvalidInput(String fieldName) {
+        if (isExcluded(fieldName)) {
+            var normalizedFieldName = normalizeSpace(fieldName);
+            LOG.debug("Form field [{}}] is rejected!", normalizedFieldName);
+            errors.add(new LocalizedMessage(getClass(), 
STRUTS_MESSAGES_UPLOAD_ERROR_ILLEGAL_CHARACTERS_FIELD, null,
+                    new String[]{normalizedFieldName}));
+            return true;
+        }
+        return false;
+    }
 }
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
index ad2544b52..a61ced6da 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java
@@ -18,16 +18,17 @@
  */
 package org.apache.struts2.dispatcher.multipart;
 
-import jakarta.servlet.http.HttpServletRequest;
 import org.apache.commons.fileupload2.core.DiskFileItem;
 import org.apache.commons.fileupload2.core.DiskFileItemFactory;
 import 
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.inject.Inject;
 
+import jakarta.servlet.http.HttpServletRequest;
 import java.io.IOException;
 import java.nio.charset.Charset;
 import java.nio.file.Path;
@@ -44,12 +45,12 @@ public class JakartaMultiPartRequest extends 
AbstractMultiPartRequest {
     private static final Logger LOG = 
LogManager.getLogger(JakartaMultiPartRequest.class);
 
     public JakartaMultiPartRequest() {
-        super(Boolean.FALSE.toString());
+        super();
     }
 
     @Inject(value = StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, 
required = false)
     public JakartaMultiPartRequest(String dmiValue) {
-        super(dmiValue);
+        super(BooleanUtils.toBoolean(dmiValue));
     }
 
     @Override
@@ -88,15 +89,14 @@ public class JakartaMultiPartRequest extends 
AbstractMultiPartRequest {
     }
 
     protected void processNormalFormField(DiskFileItem item, Charset charset) 
throws IOException {
-        LOG.debug("Item: {} is a normal form field", item.getName());
+        var fieldName = item.getFieldName();
+        LOG.debug("Item: {} is a normal form field", fieldName);
 
-        if (isExcluded(item.getFieldName())) {
-            LOG.warn(() -> "Form field [%s] is 
rejected!".formatted(normalizeSpace(item.getFieldName())));
+        if (isInvalidInput(fieldName)) {
             return;
         }
 
         List<String> values;
-        String fieldName = item.getFieldName();
         if (parameters.get(fieldName) != null) {
             values = parameters.get(fieldName);
         } else {
@@ -116,19 +116,7 @@ public class JakartaMultiPartRequest extends 
AbstractMultiPartRequest {
     }
 
     protected void processFileField(DiskFileItem item) {
-        if (isExcluded(item.getName())) {
-            LOG.warn(() -> "File name [%s] is not 
accepted".formatted(normalizeSpace(item.getName())));
-            return;
-        }
-
-        if (isExcluded(item.getFieldName())) {
-            LOG.warn(() -> "Field name [%s] is not 
accepted".formatted(normalizeSpace(item.getFieldName())));
-            return;
-        }
-
-        // Skip file uploads that don't have a file name - meaning that no 
file was selected.
-        if (item.getName() == null || item.getName().trim().isEmpty()) {
-            LOG.debug(() -> "No file has been uploaded for the field: " + 
normalizeSpace(item.getFieldName()));
+        if (isInvalidInput(item.getFieldName(), item.getName())) {
             return;
         }
 
diff --git 
a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
 
b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
index abe7ad3a6..76c0e3654 100644
--- 
a/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
+++ 
b/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequest.java
@@ -18,18 +18,19 @@
  */
 package org.apache.struts2.dispatcher.multipart;
 
-import jakarta.servlet.http.HttpServletRequest;
 import org.apache.commons.fileupload2.core.DiskFileItemFactory;
 import org.apache.commons.fileupload2.core.FileItemInput;
 import org.apache.commons.fileupload2.core.FileUploadFileCountLimitException;
 import org.apache.commons.fileupload2.core.FileUploadSizeException;
 import 
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
+import org.apache.commons.lang3.BooleanUtils;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.apache.struts2.StrutsConstants;
 import org.apache.struts2.dispatcher.LocalizedMessage;
 import org.apache.struts2.inject.Inject;
 
+import jakarta.servlet.http.HttpServletRequest;
 import java.io.BufferedOutputStream;
 import java.io.ByteArrayOutputStream;
 import java.io.File;
@@ -59,12 +60,12 @@ public class JakartaStreamMultiPartRequest extends 
AbstractMultiPartRequest {
     private static final Logger LOG = 
LogManager.getLogger(JakartaStreamMultiPartRequest.class);
 
     public JakartaStreamMultiPartRequest() {
-        super(Boolean.FALSE.toString());
+        super();
     }
 
     @Inject(value = StrutsConstants.STRUTS_ENABLE_DYNAMIC_METHOD_INVOCATION, 
required = false)
     public JakartaStreamMultiPartRequest(String dmiValue) {
-        super(dmiValue);
+        super(BooleanUtils.toBoolean(dmiValue));
     }
 
     /**
@@ -125,13 +126,11 @@ public class JakartaStreamMultiPartRequest extends 
AbstractMultiPartRequest {
      */
     protected void processFileItemAsFormField(FileItemInput fileItemInput) 
throws IOException {
         String fieldName = fileItemInput.getFieldName();
-        String fieldValue = readStream(fileItemInput.getInputStream());
-
-        if (isExcluded(fieldName)) {
-            LOG.warn(() -> "Form field [%s] is 
rejected!".formatted(normalizeSpace(fieldName)));
+        if (isInvalidInput(fieldName)) {
             return;
         }
 
+        String fieldValue = readStream(fileItemInput.getInputStream());
         if (exceedsMaxStringLength(fieldName, fieldValue)) {
             return;
         }
@@ -203,14 +202,7 @@ public class JakartaStreamMultiPartRequest extends 
AbstractMultiPartRequest {
      * @param location      location
      */
     protected void processFileItemAsFileField(FileItemInput fileItemInput, 
Path location) throws IOException {
-        // Skip file uploads that don't have a file name - meaning that no 
file was selected.
-        if (fileItemInput.getName() == null || 
fileItemInput.getName().trim().isEmpty()) {
-            LOG.debug(() -> "No file has been uploaded for the field: " + 
normalizeSpace(fileItemInput.getFieldName()));
-            return;
-        }
-
-        if (isExcluded(fileItemInput.getName())) {
-            LOG.warn(() -> "File field [%s] 
rejected".formatted(normalizeSpace(fileItemInput.getName())));
+        if (isInvalidInput(fileItemInput.getFieldName(), 
fileItemInput.getName())) {
             return;
         }
 
diff --git 
a/core/src/main/resources/org/apache/struts2/struts-messages.properties 
b/core/src/main/resources/org/apache/struts2/struts-messages.properties
index 7c418f75c..e802b5cc4 100644
--- a/core/src/main/resources/org/apache/struts2/struts-messages.properties
+++ b/core/src/main/resources/org/apache/struts2/struts-messages.properties
@@ -40,6 +40,10 @@ struts.messages.error.file.too.large=The file is too large 
to be uploaded: {0} "
 # 1 - max string length
 # 2 - actual size
 struts.messages.upload.error.parameter.too.long=The request parameter "{0}" 
was too long. Max length allowed is {1}, but found {2}!
+# 0 - field name
+struts.messages.upload.error.illegal.characters.field=The multipart upload 
field name "{0}" contains illegal characters!
+# 0 - file name
+struts.messages.upload.error.illegal.characters.name=The multipart upload 
filename "{0}" contains illegal characters!
 # 0 - input name
 # 1 - original filename
 # 2 - file name after uploading the file
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java
index 29143f7ef..699d0b66c 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequestTest.java
@@ -518,8 +518,8 @@ abstract class AbstractMultiPartRequestTest {
 
         multiPart.parse(mockRequest, tempDir);
 
-        assertThat(multiPart.getErrors())
-                .isEmpty();
+        
assertThat(multiPart.getErrors()).extracting(LocalizedMessage::getTextKey)
+                
.containsExactly(AbstractMultiPartRequest.STRUTS_MESSAGES_UPLOAD_ERROR_ILLEGAL_CHARACTERS_FIELD);
 
         assertThat(multiPart.getParameterNames().asIterator()).toIterable()
                 .isEmpty();
@@ -537,8 +537,8 @@ abstract class AbstractMultiPartRequestTest {
 
         multiPart.parse(mockRequest, tempDir);
 
-        assertThat(multiPart.getErrors())
-                .isEmpty();
+        
assertThat(multiPart.getErrors()).extracting(LocalizedMessage::getTextKey)
+                
.containsExactly(AbstractMultiPartRequest.STRUTS_MESSAGES_UPLOAD_ERROR_ILLEGAL_CHARACTERS_NAME);
 
         assertThat(multiPart.getParameterNames().asIterator()).toIterable()
                 .hasSize(1);

Reply via email to