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

lukaszlenart pushed a commit to branch feature/WW-5501-exclude-s7
in repository https://gitbox.apache.org/repos/asf/struts.git

commit a10dc21f14c04d3a469275d37fc170bbed260978
Author: Lukasz Lenart <lukaszlen...@apache.org>
AuthorDate: Mon Dec 23 11:43:47 2024 +0100

    WW-5501 Excludes malicious names
---
 .../multipart/AbstractMultiPartRequest.java        | 12 ++++
 .../multipart/JakartaMultiPartRequest.java         | 15 +++++
 .../multipart/JakartaStreamMultiPartRequest.java   |  5 ++
 .../security/DefaultExcludedPatternsChecker.java   |  3 +-
 .../multipart/AbstractMultiPartRequestTest.java    | 16 +++++-
 .../multipart/JakartaMultiPartRequestTest.java     |  6 +-
 .../JakartaStreamMultiPartRequestTest.java         |  5 +-
 .../ActionFileUploadInterceptorTest.java           | 66 +++++++++++++++++++++-
 .../DefaultExcludedPatternsCheckerTest.java        |  2 +-
 9 files changed, 124 insertions(+), 6 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 ccb25e053..38852a2f2 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
@@ -31,6 +31,7 @@ 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.security.NotExcludedAcceptedPatternsChecker;
 
 import java.io.IOException;
 import java.nio.charset.Charset;
@@ -107,6 +108,8 @@ public abstract class AbstractMultiPartRequest implements 
MultiPartRequest {
      */
     protected Map<String, List<String>> parameters = new HashMap<>();
 
+    protected NotExcludedAcceptedPatternsChecker patternsChecker;
+
     /**
      * @param bufferSize Sets the buffer size to be used.
      */
@@ -180,6 +183,11 @@ public abstract class AbstractMultiPartRequest implements 
MultiPartRequest {
         return Charset.forName(charsetStr);
     }
 
+    @Inject
+    public void 
setNotExcludedAllowedPatternsChecker(NotExcludedAcceptedPatternsChecker 
patternsChecker) {
+        this.patternsChecker = patternsChecker;
+    }
+
     /**
      * Creates an instance of {@link JakartaServletDiskFileUpload} used by the 
parser to extract uploaded files
      *
@@ -413,4 +421,8 @@ public abstract class AbstractMultiPartRequest implements 
MultiPartRequest {
         }
     }
 
+    protected boolean isAccepted(String fileName) {
+        return patternsChecker.isAllowed(fileName).isAllowed();
+    }
+
 }
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 3a76adc40..ffb3a48d5 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
@@ -77,6 +77,11 @@ public class JakartaMultiPartRequest extends 
AbstractMultiPartRequest {
     protected void processNormalFormField(DiskFileItem item, Charset charset) 
throws IOException {
         LOG.debug("Item: {} is a normal form field", item.getName());
 
+        if (!isAccepted(item.getFieldName())) {
+            LOG.warn("Form field name [{}] is not accepted", 
item.getFieldName().replaceAll("[\\r\\n]", ""));
+            return;
+        }
+
         List<String> values;
         String fieldName = item.getFieldName();
         if (parameters.get(fieldName) != null) {
@@ -98,6 +103,16 @@ public class JakartaMultiPartRequest extends 
AbstractMultiPartRequest {
     }
 
     protected void processFileField(DiskFileItem item) {
+        if (!isAccepted(item.getName())) {
+            LOG.warn("File name [{}] is not accepted", 
item.getName().replaceAll("[\\r\\n]", ""));
+            return;
+        }
+
+        if (!isAccepted(item.getFieldName())) {
+            LOG.warn("Field name [{}] is not accepted", 
item.getFieldName().replaceAll("[\\r\\n]", ""));
+            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: " + 
sanitizeNewlines(item.getFieldName()));
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 cebd60250..2c4ca9266 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
@@ -191,6 +191,11 @@ public class JakartaStreamMultiPartRequest extends 
AbstractMultiPartRequest {
             return;
         }
 
+        if (!isAccepted(fileItemInput.getName())) {
+            LOG.warn("File field [{}] rejected", fileItemInput.getName());
+            return;
+        }
+
         if (exceedsMaxFiles(fileItemInput)) {
             return;
         }
diff --git 
a/core/src/main/java/org/apache/struts2/security/DefaultExcludedPatternsChecker.java
 
b/core/src/main/java/org/apache/struts2/security/DefaultExcludedPatternsChecker.java
index 824d98c58..6b7f5eed4 100644
--- 
a/core/src/main/java/org/apache/struts2/security/DefaultExcludedPatternsChecker.java
+++ 
b/core/src/main/java/org/apache/struts2/security/DefaultExcludedPatternsChecker.java
@@ -36,7 +36,8 @@ public class DefaultExcludedPatternsChecker implements 
ExcludedPatternsChecker {
     private static final Logger LOG = 
LogManager.getLogger(DefaultExcludedPatternsChecker.class);
 
     public static final String[] EXCLUDED_PATTERNS = {
-            
"(^|\\%\\{)((#?)(top(\\.|\\['|\\[\")|\\[\\d\\]\\.)?)(dojo|struts|session|request|response|application|servlet(Request|Response|Context)|parameters|context|_memberAccess)(\\.|\\[).*",
+            "(^|\\%\\{)(#?top\\.)[^\\s]*",
+            
"(^|\\%\\{)((#?)(\\[\\d\\]\\.)?)(dojo|struts|session|request|response|application|servlet(Request|Response|Context)|parameters|context|_memberAccess)(\\.|\\[).*",
             ".*(^|\\.|\\[|\\'|\"|get)class(\\(\\.|\\[|\\'|\").*",
             "actionErrors|actionMessages|fieldErrors"
     };
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 22625cdf9..6186208c1 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
@@ -19,7 +19,13 @@
 package org.apache.struts2.dispatcher.multipart;
 
 import 
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
+import org.apache.struts2.config.Configuration;
+import org.apache.struts2.config.ConfigurationManager;
+import org.apache.struts2.dispatcher.Dispatcher;
 import org.apache.struts2.dispatcher.LocalizedMessage;
+import org.apache.struts2.inject.Container;
+import org.apache.struts2.util.StrutsTestCaseHelper;
+import org.apache.struts2.views.jsp.StrutsMockServletContext;
 import org.assertj.core.api.InstanceOfAssertFactories;
 import org.junit.After;
 import org.junit.Before;
@@ -31,6 +37,7 @@ import java.io.File;
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
@@ -47,6 +54,7 @@ abstract class AbstractMultiPartRequestTest {
     protected final String endline = "\r\n";
 
     protected AbstractMultiPartRequest multiPart;
+    protected Container container;
 
     abstract protected AbstractMultiPartRequest createMultipartRequest();
 
@@ -59,7 +67,13 @@ abstract class AbstractMultiPartRequestTest {
     }
 
     @Before
-    public void before() {
+    public void before() throws Exception {
+        StrutsMockServletContext servletContext = new 
StrutsMockServletContext();
+        Dispatcher dispatcher = 
StrutsTestCaseHelper.initDispatcher(servletContext, Collections.emptyMap());
+        ConfigurationManager configurationManager = 
dispatcher.getConfigurationManager();
+        Configuration configuration = configurationManager.getConfiguration();
+        container = configuration.getContainer();
+
         mockRequest = new MockHttpServletRequest();
         mockRequest.setCharacterEncoding(StandardCharsets.UTF_8.name());
         mockRequest.setMethod("post");
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java
index 781b7fbd0..efa0ff9b0 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequestTest.java
@@ -18,11 +18,15 @@
  */
 package org.apache.struts2.dispatcher.multipart;
 
+import org.apache.struts2.security.DefaultNotExcludedAcceptedPatternsChecker;
+
 public class JakartaMultiPartRequestTest extends AbstractMultiPartRequestTest {
 
     @Override
     protected AbstractMultiPartRequest createMultipartRequest() {
-        return new JakartaMultiPartRequest();
+        JakartaMultiPartRequest multiPartRequest = new 
JakartaMultiPartRequest();
+        
multiPartRequest.setNotExcludedAllowedPatternsChecker(container.inject(DefaultNotExcludedAcceptedPatternsChecker.class));
+        return multiPartRequest;
     }
 
 }
diff --git 
a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java
 
b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java
index aa54316f5..532be8a38 100644
--- 
a/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java
+++ 
b/core/src/test/java/org/apache/struts2/dispatcher/multipart/JakartaStreamMultiPartRequestTest.java
@@ -20,6 +20,7 @@ package org.apache.struts2.dispatcher.multipart;
 
 import 
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
 import org.apache.struts2.dispatcher.LocalizedMessage;
+import org.apache.struts2.security.DefaultNotExcludedAcceptedPatternsChecker;
 import org.assertj.core.api.InstanceOfAssertFactories;
 import org.junit.Test;
 
@@ -32,7 +33,9 @@ public class JakartaStreamMultiPartRequestTest extends 
AbstractMultiPartRequestT
 
     @Override
     protected AbstractMultiPartRequest createMultipartRequest() {
-        return new JakartaStreamMultiPartRequest();
+        JakartaStreamMultiPartRequest multiPartRequest = new 
JakartaStreamMultiPartRequest();
+        
multiPartRequest.setNotExcludedAllowedPatternsChecker(container.inject(DefaultNotExcludedAcceptedPatternsChecker.class));
+        return multiPartRequest;
     }
 
     @Test
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 93e52ab11..9aca62717 100644
--- 
a/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java
+++ 
b/core/src/test/java/org/apache/struts2/interceptor/ActionFileUploadInterceptorTest.java
@@ -24,6 +24,7 @@ import org.apache.struts2.locale.DefaultLocaleProvider;
 import org.apache.struts2.ValidationAwareSupport;
 import org.apache.struts2.mock.MockActionInvocation;
 import org.apache.struts2.mock.MockActionProxy;
+import org.apache.struts2.security.DefaultNotExcludedAcceptedPatternsChecker;
 import org.apache.struts2.util.ClassLoaderUtil;
 import 
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletDiskFileUpload;
 import 
org.apache.commons.fileupload2.jakarta.servlet6.JakartaServletFileUpload;
@@ -514,11 +515,71 @@ public class ActionFileUploadInterceptorTest extends 
StrutsInternalTestCase {
         assertTrue(msg.startsWith("Der Request übertraf die maximal erlaubte 
Größe"));
     }
 
+    public void testUnacceptedFieldName() throws Exception {
+        request.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        request.setMethod("post");
+        request.addHeader("Content-type", "multipart/form-data; 
boundary=---1234");
+
+        // inspired by the unit tests for jakarta commons fileupload
+        String content = ("-----1234\r\n" +
+            "Content-Disposition: form-data; name=\"top.file\"; 
filename=\"deleteme.txt\"\r\n" +
+            "Content-Type: text/html\r\n" +
+            "\r\n" +
+            "Unit test of ActionFileUploadInterceptor" +
+            "\r\n" +
+            "-----1234--\r\n");
+        request.setContent(content.getBytes(StandardCharsets.US_ASCII));
+
+        MyFileUploadAction action = container.inject(MyFileUploadAction.class);
+
+        MockActionInvocation mai = new MockActionInvocation();
+        mai.setAction(action);
+        mai.setResultCode("success");
+        mai.setInvocationContext(ActionContext.getContext());
+        ActionContext.getContext()
+            .withServletRequest(createMultipartRequestMaxSize(2000));
+
+        interceptor.intercept(mai);
+
+        assertFalse(action.hasActionErrors());
+        assertNull(action.getUploadFiles());
+    }
+
+    public void testUnacceptedFileName() throws Exception {
+        request.setCharacterEncoding(StandardCharsets.UTF_8.name());
+        request.setMethod("post");
+        request.addHeader("Content-type", "multipart/form-data; 
boundary=---1234");
+
+        // inspired by the unit tests for jakarta commons fileupload
+        String content = ("-----1234\r\n" +
+            "Content-Disposition: form-data; name=\"file\"; 
filename=\"../deleteme.txt\"\r\n" +
+            "Content-Type: text/html\r\n" +
+            "\r\n" +
+            "Unit test of ActionFileUploadInterceptor" +
+            "\r\n" +
+            "-----1234--\r\n");
+        request.setContent(content.getBytes(StandardCharsets.US_ASCII));
+
+        MyFileUploadAction action = container.inject(MyFileUploadAction.class);
+
+        MockActionInvocation mai = new MockActionInvocation();
+        mai.setAction(action);
+        mai.setResultCode("success");
+        mai.setInvocationContext(ActionContext.getContext());
+        ActionContext.getContext()
+            .withServletRequest(createMultipartRequestMaxSize(2000));
+
+        interceptor.intercept(mai);
+
+        assertFalse(action.hasActionErrors());
+        assertNull(action.getUploadFiles());
+    }
+
     private String encodeTextFile(String filename, String contentType, String 
content) {
         return endline +
                 "--" + boundary +
                 endline +
-                "Content-Disposition: form-data; name=\"" + "file" + "\"; 
filename=\"" + filename +
+                "Content-Disposition: form-data; name=\"" + "file" + "\"; 
filename=\"" + filename + "\"" +
                 endline +
                 "Content-Type: " + contentType +
                 endline +
@@ -549,6 +610,9 @@ public class ActionFileUploadInterceptorTest extends 
StrutsInternalTestCase {
         jak.setMaxFiles(String.valueOf(maxfiles));
         jak.setMaxStringLength(String.valueOf(maxStringLength));
         jak.setDefaultEncoding(StandardCharsets.UTF_8.name());
+        DefaultNotExcludedAcceptedPatternsChecker patternsChecker = 
container.inject(DefaultNotExcludedAcceptedPatternsChecker.class);
+        jak.setNotExcludedAllowedPatternsChecker(patternsChecker);
+
         return new MultiPartRequestWrapper(jak, request, 
tempDir.getAbsolutePath(), new DefaultLocaleProvider());
     }
 
diff --git 
a/core/src/test/java/org/apache/struts2/security/DefaultExcludedPatternsCheckerTest.java
 
b/core/src/test/java/org/apache/struts2/security/DefaultExcludedPatternsCheckerTest.java
index 1f549e011..06889302a 100644
--- 
a/core/src/test/java/org/apache/struts2/security/DefaultExcludedPatternsCheckerTest.java
+++ 
b/core/src/test/java/org/apache/struts2/security/DefaultExcludedPatternsCheckerTest.java
@@ -93,7 +93,7 @@ public class DefaultExcludedPatternsCheckerTest extends 
XWorkTestCase {
 
     public void testDefaultExcludePatterns() throws Exception {
         // given
-        List<String> prefixes = Arrays.asList("#[0].%s", "[0].%s", "top.%s", 
"%{[0].%s}", "%{#[0].%s}", "%{top.%s}", "%{#top.%s}", "%{#%s}", "%{%s}", "#%s");
+        List<String> prefixes = Arrays.asList("#[0].%s", "[0].%s", "top.%s", 
"%{[0].%s}", "%{#[0].%s}", "%{top.%s}", "%{#top.%s}", "%{#%s}", "%{%s}", "#%s", 
"top.param", "%{top.request}", "#top.param");
         List<String> inners = Arrays.asList("servletRequest", 
"servletResponse", "servletContext", "application", "session", "struts", 
"request", "response", "dojo", "parameters");
         List<String> suffixes = Arrays.asList("['test']", "[\"test\"]", 
".test");
 

Reply via email to