This is an automated email from the ASF dual-hosted git repository. kusal pushed a commit to branch WW-5528-multipart-illegal-char-errors-67 in repository https://gitbox.apache.org/repos/asf/struts.git
commit e16d24b146b30629710a78946834baa02cb15c9a Author: Kusal Kithul-Godage <g...@kusal.io> AuthorDate: Thu Feb 6 18:49:34 2025 +1100 WW-5528 Ensure multipart upload illegal characters reported as error --- .../multipart/AbstractMultiPartRequest.java | 61 +++++++++++++++------- .../multipart/JakartaMultiPartRequest.java | 28 +++------- .../multipart/JakartaStreamMultiPartRequest.java | 17 ++---- .../org/apache/struts2/struts-messages.properties | 2 + .../ActionFileUploadInterceptorTest.java | 9 ++-- .../interceptor/FileUploadInterceptorTest.java | 7 +-- 6 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 1511096b0..b522a4532 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 @@ -22,7 +22,7 @@ import com.opensymphony.xwork2.LocaleProviderFactory; import com.opensymphony.xwork2.inject.Inject; import com.opensymphony.xwork2.security.DefaultExcludedPatternsChecker; import com.opensymphony.xwork2.security.ExcludedPatternsChecker; -import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.io.FilenameUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.StrutsConstants; @@ -33,12 +33,17 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +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} */ public abstract class AbstractMultiPartRequest implements MultiPartRequest { + 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); private static final String EXCLUDED_FILE_PATTERN = "^(.*[<>&\"'|;\\\\/?*:]+.*|.*\\.\\..*)$"; @@ -88,13 +93,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) { + DefaultExcludedPatternsChecker patternsChecker = new DefaultExcludedPatternsChecker(); + patternsChecker.setAdditionalExcludePatterns(dmiValue ? EXCLUDED_FILE_PATTERN_WITH_DMI_SUPPORT : EXCLUDED_FILE_PATTERN); + this.patternsChecker = patternsChecker; } /** @@ -174,16 +180,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); } /** @@ -194,4 +191,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.trim().isEmpty()) { + LOG.debug(() -> "No file has been uploaded for the field: " + normalizeSpace(fieldName)); + return true; + } + + if (isExcluded(fileName)) { + String 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)) { + String 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 8ed908ff9..6bbfffec8 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 @@ -27,6 +27,7 @@ import org.apache.commons.fileupload.RequestContext; import org.apache.commons.fileupload.disk.DiskFileItem; import org.apache.commons.fileupload.disk.DiskFileItemFactory; import org.apache.commons.fileupload.servlet.ServletFileUpload; +import org.apache.commons.lang3.BooleanUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -60,13 +61,14 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest { // maps parameter name -> List of param values protected Map<String, List<String>> params = new HashMap<>(); + 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)); } /** @@ -123,21 +125,7 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest { } protected void processFileField(FileItem item) { - LOG.debug("Item is a file upload"); - - if (isExcluded(item.getName())) { - LOG.warn("File name [{}] is not accepted", normalizeSpace(item.getName())); - return; - } - - if (isExcluded(item.getFieldName())) { - LOG.warn("Field name [{}] is not accepted", 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; } @@ -154,10 +142,10 @@ public class JakartaMultiPartRequest extends AbstractMultiPartRequest { protected void processNormalFormField(FileItem item, String charset) throws UnsupportedEncodingException { try { - LOG.debug("Item is a normal form field"); + String fieldName = item.getFieldName(); + LOG.debug("Item: {} is a normal form field", fieldName); - if (isExcluded(item.getFieldName())) { - LOG.warn("Form field name [{}] is not accepted", normalizeSpace(item.getFieldName())); + if (isInvalidInput(fieldName)) { 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 cb7475f40..655112010 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 @@ -26,6 +26,7 @@ import org.apache.commons.fileupload.FileUploadBase.FileSizeLimitExceededExcepti import org.apache.commons.fileupload.FileUploadException; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.commons.fileupload.util.Streams; +import org.apache.commons.lang3.BooleanUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.struts2.StrutsConstants; @@ -209,12 +210,12 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { } 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)); } /** @@ -325,8 +326,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { */ protected void processFileItemStreamAsFormField(FileItemStream itemStream) { String fieldName = itemStream.getFieldName(); - if (isExcluded(fieldName)) { - LOG.warn("Form field [{}] rejected!", normalizeSpace(fieldName)); + if (isInvalidInput(fieldName)) { return; } try { @@ -351,14 +351,7 @@ public class JakartaStreamMultiPartRequest extends AbstractMultiPartRequest { * @param location location */ protected void processFileItemStreamAsFileField(FileItemStream itemStream, String location) { - // Skip file uploads that don't have a file name - meaning that no file was selected. - if (itemStream.getName() == null || itemStream.getName().trim().isEmpty()) { - LOG.debug("No file has been uploaded for the field: {}", normalizeSpace(itemStream.getFieldName())); - return; - } - - if (isExcluded(itemStream.getName())) { - LOG.warn("File field [{}] rejected", normalizeSpace(itemStream.getName())); + if (isInvalidInput(itemStream.getFieldName(), itemStream.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 57b7e2b54..b75a4144e 100644 --- a/core/src/main/resources/org/apache/struts2/struts-messages.properties +++ b/core/src/main/resources/org/apache/struts2/struts-messages.properties @@ -27,6 +27,8 @@ struts.messages.removing.file=Removing file {0} {1} struts.messages.error.uploading=Error uploading: {0} struts.messages.error.file.too.large=File {0} is too large to be uploaded. Maximum allowed size is {4} bytes! struts.messages.upload.error.parameter.too.long=The request parameter "{0}" was too long. Max length allowed is {1}, but found {2}! +struts.messages.upload.error.illegal.characters.field=The multipart upload field name "{0}" contains illegal characters! +struts.messages.upload.error.illegal.characters.name=The multipart upload filename "{0}" contains illegal characters! struts.messages.error.content.type.not.allowed=Content-Type not allowed: {0} "{1}" "{2}" {3} struts.messages.error.file.extension.not.allowed=File extension not allowed: {0} "{1}" "{2}" {3} diff --git a/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java index b9e64df8f..85bf5ab4b 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java @@ -24,9 +24,6 @@ import com.opensymphony.xwork2.DefaultLocaleProvider; import com.opensymphony.xwork2.ValidationAwareSupport; import com.opensymphony.xwork2.mock.MockActionInvocation; import com.opensymphony.xwork2.mock.MockActionProxy; -import com.opensymphony.xwork2.security.DefaultAcceptedPatternsChecker; -import com.opensymphony.xwork2.security.DefaultExcludedPatternsChecker; -import com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsChecker; import com.opensymphony.xwork2.util.ClassLoaderUtil; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.struts2.ServletActionContext; @@ -693,7 +690,8 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase { interceptor.intercept(mai); - assertFalse(action.hasActionErrors()); + assertThat(action.getActionErrors()) + .containsExactly("The multipart upload field name \"top.file\" contains illegal characters!"); assertNull(action.getUploadFiles()); } @@ -724,7 +722,8 @@ public class ActionFileUploadInterceptorTest extends StrutsInternalTestCase { interceptor.intercept(mai); - assertFalse(action.hasActionErrors()); + assertThat(action.getActionErrors()) + .containsExactly("The multipart upload filename \"../deleteme.txt\" contains illegal characters!"); assertNull(action.getUploadFiles()); } diff --git a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java index 0df273321..fe4c2ef21 100644 --- a/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java +++ b/core/src/test/java/org/apache/struts2/interceptor/FileUploadInterceptorTest.java @@ -23,7 +23,6 @@ import com.opensymphony.xwork2.ActionSupport; import com.opensymphony.xwork2.DefaultLocaleProvider; import com.opensymphony.xwork2.ValidationAwareSupport; import com.opensymphony.xwork2.mock.MockActionInvocation; -import com.opensymphony.xwork2.security.DefaultNotExcludedAcceptedPatternsChecker; import com.opensymphony.xwork2.util.ClassLoaderUtil; import org.apache.commons.fileupload.servlet.ServletFileUpload; import org.apache.struts2.ServletActionContext; @@ -755,7 +754,8 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { interceptor.intercept(mai); - assertFalse(action.hasActionErrors()); + assertThat(action.getActionErrors()) + .containsExactly("The multipart upload field name \"top.file\" contains illegal characters!"); assertNull(action.getUploadFiles()); } @@ -786,7 +786,8 @@ public class FileUploadInterceptorTest extends StrutsInternalTestCase { interceptor.intercept(mai); - assertFalse(action.hasActionErrors()); + assertThat(action.getActionErrors()) + .containsExactly("The multipart upload filename \"../deleteme.txt\" contains illegal characters!"); assertNull(action.getUploadFiles()); }