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

markt pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 6197c94cf6dd16a3eae5bb33315f42a17da1987a
Author: Mark Thomas <ma...@apache.org>
AuthorDate: Wed Jun 4 10:31:42 2025 +0100

    Add support for new attributes to ParameterLimitValve
---
 java/org/apache/catalina/connector/Request.java    |  40 +++++++-
 .../apache/catalina/valves/LocalStrings.properties |  12 ++-
 .../catalina/valves/ParameterLimitValve.java       |  59 +++++++-----
 .../catalina/valves/TestParameterLimitValve.java   | 105 ++++++++++++++++++++-
 .../valves/TestParameterLimitValveConfig.java      |  44 +++++++++
 webapps/docs/changelog.xml                         |   3 +-
 webapps/docs/config/valve.xml                      |   5 +
 7 files changed, 231 insertions(+), 37 deletions(-)

diff --git a/java/org/apache/catalina/connector/Request.java 
b/java/org/apache/catalina/connector/Request.java
index 4fefd870b4..627e326932 100644
--- a/java/org/apache/catalina/connector/Request.java
+++ b/java/org/apache/catalina/connector/Request.java
@@ -145,7 +145,9 @@ public class Request implements HttpServletRequest {
     public Request(Connector connector, org.apache.coyote.Request 
coyoteRequest) {
         this.connector = connector;
         if (connector != null) {
-            this.maxParameterCount = connector.getMaxParameterCount();
+            maxParameterCount = connector.getMaxParameterCount();
+            maxPartCount = connector.getMaxPartCount();
+            maxPartHeaderSize = connector.getMaxPartHeaderSize();
         }
         this.coyoteRequest = coyoteRequest;
         inputBuffer = new InputBuffer(coyoteRequest);
@@ -418,6 +420,10 @@ public class Request implements HttpServletRequest {
      */
     private int maxParameterCount = -1;
 
+    private int maxPartCount = -1;
+
+    private int maxPartHeaderSize = -1;
+
     // --------------------------------------------------------- Public Methods
 
     public void addPathParameter(String name, String value) {
@@ -449,8 +455,12 @@ public class Request implements HttpServletRequest {
         parametersParsed = false;
         if (connector != null) {
             maxParameterCount = connector.getMaxParameterCount();
+            maxPartCount = connector.getMaxPartCount();
+            maxPartHeaderSize = connector.getMaxPartHeaderSize();
         } else {
             maxParameterCount = -1;
+            maxPartCount = -1;
+            maxPartHeaderSize = -1;
         }
         if (parts != null) {
             for (Part part : parts) {
@@ -840,8 +850,9 @@ public class Request implements HttpServletRequest {
         coyoteRequest.setServerPort(port);
     }
 
+
     /**
-     * Set the maximum number of request parameters (GET plus POST) for a 
single request
+     * Set the maximum number of request parameters (GET plus POST including 
multipart) for a single request.
      *
      * @param maxParameterCount The maximum number of request parameters
      */
@@ -849,6 +860,27 @@ public class Request implements HttpServletRequest {
         this.maxParameterCount = maxParameterCount;
     }
 
+
+    /**
+     * Set the maximum number of parts for a single multipart request.
+     *
+     * @param maxPartCount The maximum number of request parts
+     */
+    public void setMaxPartCount(int maxPartCount) {
+        this.maxPartCount = maxPartCount;
+    }
+
+
+    /**
+     * Set the maximum header size per part for a single multipart request.
+     *
+     * @param maxPartHeaderSize The maximum size of the headers for one part
+     */
+    public void setMaxPartHeaderSize(int maxPartHeaderSize) {
+        this.maxPartHeaderSize = maxPartHeaderSize;
+    }
+
+
     // ------------------------------------------------- ServletRequest Methods
 
     @Override
@@ -2382,7 +2414,7 @@ public class Request implements HttpServletRequest {
         upload.setFileItemFactory(factory);
         upload.setFileSizeMax(mce.getMaxFileSize());
         upload.setSizeMax(mce.getMaxRequestSize());
-        upload.setPartHeaderSizeMax(connector.getMaxPartHeaderSize());
+        upload.setPartHeaderSizeMax(maxPartHeaderSize);
         /*
          * There are two independent limits on the number of parts.
          *
@@ -2398,7 +2430,7 @@ public class Request implements HttpServletRequest {
         if (partLimit > -1) {
             partLimit = partLimit - parameters.size();
         }
-        int maxPartCount = connector.getMaxPartCount();
+        int maxPartCount = this.maxPartCount;
         if (maxPartCount > -1) {
             if (partLimit < 0 || partLimit > maxPartCount) {
                 partLimit = maxPartCount;
diff --git a/java/org/apache/catalina/valves/LocalStrings.properties 
b/java/org/apache/catalina/valves/LocalStrings.properties
index 553fd4d330..35a458f4d4 100644
--- a/java/org/apache/catalina/valves/LocalStrings.properties
+++ b/java/org/apache/catalina/valves/LocalStrings.properties
@@ -139,6 +139,13 @@ loadBalancerDrainingValve.skip=Client is presenting a 
valid [{0}] cookie, re-bal
 
 patternTokenizer.unexpectedParenthesis=Unexpected ')' in pattern
 
+parameterLimitValve.closeError=Error closing configuration
+parameterLimitValve.invalidLimits=Each limit configuration must contain either 
a single integer or three, comma-separated integers. Invalid limit string [{0}]
+parameterLimitValve.invalidLine=Each line must contain at least one '=' 
character. Invalid line [{0}]
+parameterLimitValve.noConfiguration=No configuration resource found [{0}]
+parameterLimitValve.readConfiguration=Read configuration from [/WEB-INF/{0}]
+parameterLimitValve.readError=Error reading 
configurationpatternTokenizer.unexpectedParenthesis=Unexpected ')' in pattern
+
 persistentValve.acquireFailed=The request for [{0}] did not obtain the per 
session Semaphore as no permit was available
 persistentValve.acquireInterrupted=The request for [{0}] did not obtain the 
per session Semaphore as it was interrupted while waiting for a permit
 persistentValve.filter.failure=Unable to compile filter=[{0}]
@@ -169,8 +176,3 @@ stuckThreadDetectionValve.notifyStuckThreadCompleted=Thread 
[{0}] (id=[{3}]) was
 stuckThreadDetectionValve.notifyStuckThreadDetected=Thread [{0}] (id=[{6}]) 
has been active for [{1}] milliseconds (since [{2}]) to serve the same request 
for [{4}] and may be stuck (configured threshold for this 
StuckThreadDetectionValve is [{5}] seconds). There is/are [{3}] thread(s) in 
total that are monitored by this Valve and may be stuck.
 stuckThreadDetectionValve.notifyStuckThreadInterrupted=Thread [{0}] (id=[{5}]) 
has been interrupted because it was active for [{1}] milliseconds (since [{2}]) 
to serve the same request for [{3}] and was probably stuck (configured 
interruption threshold for this StuckThreadDetectionValve is [{4}] seconds).
 
-parameterLimitValve.closeError=Error closing configuration
-parameterLimitValve.invalidLine=Each line must contain at least one '=' 
character. Invalid line [{0}]
-parameterLimitValve.noConfiguration=No configuration resource found [{0}]
-parameterLimitValve.readConfiguration=Read configuration from [/WEB-INF/{0}]
-parameterLimitValve.readError=Error reading configuration
\ No newline at end of file
diff --git a/java/org/apache/catalina/valves/ParameterLimitValve.java 
b/java/org/apache/catalina/valves/ParameterLimitValve.java
index 67fb456ee0..8b6ef86424 100644
--- a/java/org/apache/catalina/valves/ParameterLimitValve.java
+++ b/java/org/apache/catalina/valves/ParameterLimitValve.java
@@ -38,10 +38,10 @@ import org.apache.tomcat.util.buf.UDecoder;
 import org.apache.tomcat.util.file.ConfigFileLoader;
 import org.apache.tomcat.util.file.ConfigurationSource;
 
-
 /**
- * This is a concrete implementation of {@link ValveBase} that enforces a 
limit on the number of HTTP request
- * parameters. The features of this implementation include:
+ * This is a concrete implementation of {@link ValveBase} that allows 
alternative values for the
+ * <strong>Connector</strong> attributes {@code maxParameterCount}, {@code 
maxPartCount} and {@code maxPartHeaderSize}
+ * to be applied to a request. The features of this implementation include:
  * <ul>
  * <li>URL-specific parameter limits that can be defined using regular 
expressions</li>
  * <li>Configurable through Tomcat's <code>server.xml</code> or 
<code>context.xml</code></li>
@@ -52,11 +52,9 @@ import org.apache.tomcat.util.file.ConfigurationSource;
  * The default limit, specified by Connector's value, applies to all requests 
unless a more specific URL pattern is
  * matched. URL patterns and their corresponding limits can be configured via 
a regular expression mapping through the
  * <code>urlPatternLimits</code> attribute.
- * </p>
  * <p>
  * The Valve checks each incoming request and enforces the appropriate limit. 
If a request exceeds the allowed number of
  * parameters, a <code>400 Bad Request</code> response is returned.
- * </p>
  * <p>
  * Example, configuration in <code>context.xml</code>:
  *
@@ -73,33 +71,36 @@ import org.apache.tomcat.util.file.ConfigurationSource;
  * {@code
  * /api/.*=150
  * /admin/.*=50
+ * /upload/.*=30,5,1024
  * }
  * </pre>
  * <p>
  * The configuration allows for flexible control over different sections of 
your application, such as applying higher
  * limits for API endpoints and stricter limits for admin areas.
- * </p>
- *
- * @author Dimitris Soumis
+ * <p>
+ * If a single integer is provided, it is used for {@code maxParameterCount}.
+ * <p>
+ * If three integers are provided, they are applied to {@code 
maxParameterCount}, {@code maxPartCount} and
+ * {@code maxPartHeaderSize} respectively.
  */
 
 public class ParameterLimitValve extends ValveBase {
 
     /**
-     * Map for URL-specific limits
+     * Map for URL-specific limits.
      */
-    protected Map<Pattern,Integer> urlPatternLimits = new 
ConcurrentHashMap<>();
+    private Map<Pattern,Integer[]> urlPatternLimits = new 
ConcurrentHashMap<>();
 
     /**
      * Relative path to the configuration file. Note: If the valve's container 
is a context, this will be relative to
      * /WEB-INF/.
      */
-    protected String resourcePath = "parameter_limit.config";
+    private String resourcePath = "parameter_limit.config";
 
     /**
      * Will be set to true if the valve is associated with a context.
      */
-    protected boolean context = false;
+    private boolean context = false;
 
     public ParameterLimitValve() {
         super(true);
@@ -211,14 +212,22 @@ public class ParameterLimitValve extends ValveBase {
                 }
 
                 String patternString = line.substring(0, 
lastEqualsIndex).trim();
-                String limitString = line.substring(lastEqualsIndex + 
1).trim();
+                String limitsString = line.substring(lastEqualsIndex + 
1).trim();
 
                 Pattern pattern = 
Pattern.compile(UDecoder.URLDecode(patternString, StandardCharsets.UTF_8));
-                int limit = Integer.parseInt(limitString);
+                String[] limits = limitsString.split(",");
+                if (limits.length == 1) {
+                    urlPatternLimits.put(pattern, new Integer[] { 
Integer.valueOf(limits[0]), null, null});
+                } else if (limits.length == 3) {
+                    urlPatternLimits.put(pattern, new Integer[] {
+                            Integer.valueOf(limits[0]), 
Integer.valueOf(limits[1]), Integer.valueOf(limits[2])});
+                } else {
+                    throw new IllegalArgumentException(
+                            
sm.getString("parameterLimitValve.invalidLimitsString", limitsString));
+                }
                 if (containerLog != null && containerLog.isTraceEnabled()) {
-                    containerLog.trace("Add pattern " + pattern + " and limit 
" + limit);
+                    containerLog.trace("Add pattern " + pattern + " and 
limit(s) " + limitsString);
                 }
-                urlPatternLimits.put(pattern, Integer.valueOf(limit));
             }
         } catch (IOException e) {
             containerLog.error(sm.getString("parameterLimitValve.readError"), 
e);
@@ -244,7 +253,7 @@ public class ParameterLimitValve extends ValveBase {
     @Override
     public void invoke(Request request, Response response) throws IOException, 
ServletException {
 
-        if (urlPatternLimits == null || urlPatternLimits.isEmpty()) {
+        if (urlPatternLimits.isEmpty()) {
             getNext().invoke(request, response);
             return;
         }
@@ -252,12 +261,15 @@ public class ParameterLimitValve extends ValveBase {
         String requestURI = context ? request.getRequestPathMB().toString() : 
request.getDecodedRequestURI();
 
         // Iterate over the URL patterns and apply corresponding limits
-        for (Map.Entry<Pattern,Integer> entry : urlPatternLimits.entrySet()) {
-            Pattern pattern = entry.getKey();
-            int limit = entry.getValue().intValue();
-
-            if (pattern.matcher(requestURI).matches()) {
-                request.setMaxParameterCount(limit);
+        for (Map.Entry<Pattern,Integer[]> entry : urlPatternLimits.entrySet()) 
{
+            if (entry.getKey().matcher(requestURI).matches()) {
+                Integer[] limits = entry.getValue();
+                // maxParameterCount should always be present
+                request.setMaxParameterCount(limits[0].intValue());
+                if (limits[1] != null) {
+                    request.setMaxPartCount(limits[1].intValue());
+                    request.setMaxPartHeaderSize(limits[2].intValue());
+                }
                 break;
             }
         }
@@ -265,5 +277,4 @@ public class ParameterLimitValve extends ValveBase {
         // Invoke the next valve to continue processing the request
         getNext().invoke(request, response);
     }
-
 }
diff --git a/test/org/apache/catalina/valves/TestParameterLimitValve.java 
b/test/org/apache/catalina/valves/TestParameterLimitValve.java
index fea4d644f6..178ba31fe6 100644
--- a/test/org/apache/catalina/valves/TestParameterLimitValve.java
+++ b/test/org/apache/catalina/valves/TestParameterLimitValve.java
@@ -20,23 +20,33 @@ import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileReader;
 import java.io.FileWriter;
+import java.io.IOException;
 import java.io.PrintWriter;
 import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 
-import jakarta.servlet.ServletRequestParametersBaseTest;
+import jakarta.servlet.MultipartConfigElement;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
 
 import org.junit.Assert;
 import org.junit.Test;
 
 import static org.apache.catalina.startup.SimpleHttpClient.CRLF;
 import org.apache.catalina.Context;
+import org.apache.catalina.Wrapper;
 import org.apache.catalina.core.StandardContext;
 import org.apache.catalina.startup.Tomcat;
+import org.apache.catalina.startup.TomcatBaseTest;
 import org.apache.tomcat.util.buf.ByteChunk;
 import org.apache.tomcat.util.scan.StandardJarScanner;
 
 
-public class TestParameterLimitValve extends ServletRequestParametersBaseTest {
+public class TestParameterLimitValve extends TomcatBaseTest {
 
     @Test
     public void testSpecificUrlPatternLimit() throws Exception {
@@ -251,7 +261,8 @@ public class TestParameterLimitValve extends 
ServletRequestParametersBaseTest {
     public void testUrlPatternLimitsFromFile() throws Exception {
         File configFile = File.createTempFile("parameter_limit", ".config");
         try (PrintWriter writer = new PrintWriter(new FileWriter(configFile))) 
{
-            writer.println("# Commented line");
+            writer.println("# Commented line - empty line follows");
+            writer.println("");
             writer.println("/api/.*=2");
             writer.println("# Commented line");
         }
@@ -425,4 +436,92 @@ public class TestParameterLimitValve extends 
ServletRequestParametersBaseTest {
         Assert.assertEquals(400, rc);
     }
 
+
+    @Test
+    public void testMultipart() throws Exception {
+        doTestMultipart(50,  10,  512, true);
+    }
+
+
+    @Test
+    public void testMultipartParameterLimitExceeded01() throws Exception {
+        doTestMultipart(1,  10,  512, false);
+    }
+
+
+    @Test
+    public void testMultipartParameterLimitExceeded02() throws Exception {
+        doTestMultipart(5,  10,  512, false);
+    }
+
+
+    @Test
+    public void testMultipartPartLimitExceeded() throws Exception {
+        doTestMultipart(50,  1,  512, false);
+    }
+
+
+    @Test
+    public void testMultipartPartHeaderSizeLimitExceeded() throws Exception {
+        doTestMultipart(50,  10,  1, false);
+    }
+
+
+    private void doTestMultipart(int maxParameterCount, int maxPartCount, int 
maxPartHeaderSize, boolean okExpected) throws Exception {
+
+        Tomcat tomcat = getTomcatInstance();
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        parameterLimitValve.setUrlPatternLimits("/upload/.*=" + 
Integer.toString(maxParameterCount) + "," +
+                Integer.toString(maxPartCount) + "," + 
Integer.toString(maxPartHeaderSize));
+
+        Wrapper w = Tomcat.addServlet(ctx, "multipart", new 
MultipartServlet());
+        // Use defaults for Multipart
+        w.setMultipartConfigElement(new MultipartConfigElement(""));
+        ctx.addServletMappingDecoded("/upload/*", "multipart");
+
+        tomcat.start();
+
+        // Construct a simple multipart body with two parts
+        String boundary = "--simpleBoundary";
+
+        String content = "--" + boundary + CRLF +
+                "Content-Disposition: form-data; name=\"part1\"" + CRLF + CRLF 
+
+                "part value 1" + CRLF +
+                "--" + boundary + CRLF +
+                "Content-Disposition: form-data; name=\"part2\"" + CRLF + CRLF 
+
+                "part value 2" + CRLF +                "--" + boundary + "--" 
+ CRLF;
+
+        Map<String,List<String>> reqHeaders = new HashMap<>();
+        reqHeaders.put("Content-Type", List.of("multipart/form-data; 
boundary=" + boundary));
+        reqHeaders.put("Content-Length", 
List.of(Integer.toString(content.length())));
+
+        int rc = postUrl(content.getBytes(), "http://localhost:"; + getPort() + 
"/upload/endpoint?" +
+                "param1=value1&param2=value2&param3=value3&param4=value4",
+                new ByteChunk(), reqHeaders, null);
+
+        if (okExpected) {
+            Assert.assertEquals(HttpServletResponse.SC_OK, rc);
+        } else {
+            Assert.assertTrue(Integer.toString(rc),
+                    rc == HttpServletResponse.SC_BAD_REQUEST || rc == 
HttpServletResponse.SC_REQUEST_ENTITY_TOO_LARGE);
+        }
+    }
+
+
+    private static class MultipartServlet extends HttpServlet {
+
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        protected void doPost(HttpServletRequest req, HttpServletResponse 
resp) throws ServletException, IOException {
+            resp.setContentType("text/plain");
+            resp.setCharacterEncoding(StandardCharsets.UTF_8);
+            PrintWriter pw = resp.getWriter();
+            pw.println("Parameters: " + req.getParameterMap().size());
+            pw.println("Parts: " + req.getParts().size());
+        }
+    }
 }
diff --git a/test/org/apache/catalina/valves/TestParameterLimitValveConfig.java 
b/test/org/apache/catalina/valves/TestParameterLimitValveConfig.java
new file mode 100644
index 0000000000..6bf2a4b23c
--- /dev/null
+++ b/test/org/apache/catalina/valves/TestParameterLimitValveConfig.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.catalina.valves;
+
+import org.junit.Test;
+
+public class TestParameterLimitValveConfig {
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testNoEquals() {
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        parameterLimitValve.setUrlPatternLimits("/abc");
+    }
+
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidLimitCount02() {
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        parameterLimitValve.setUrlPatternLimits("/abc=1,2");
+
+    }
+
+
+    @Test(expected = IllegalArgumentException.class)
+    public void testInvalidLimitCount04() {
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        parameterLimitValve.setUrlPatternLimits("/abc=1,2,3,4");
+
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 2024fd91c9..146dfcc151 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -228,7 +228,8 @@
         new attributes on the <code>Connector</code> element.
         <code>maxPartCount</code> limits the total number of parts in a
         multi-part request and <code>maxPartHeaderSize</code> limits the size 
of
-        the headers provided with each part. (markt)
+        the headers provided with each part. Add support for these new
+        attributes to the <code>ParameterLimitValve</code>. (markt)
       </add>
     </changelog>
   </subsection>
diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml
index 46651a8fee..8bbda41f6e 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -2706,10 +2706,15 @@
           <code>
             /api/.*=100
             /admin/.*=50
+            /upload/.*=30,5,1024
           </code>
         Default value: <code>parameter_limit.config</code>.
         It must be placed in the Host configuration folder or in the WEB-INF 
folder of the web application.
         </p>
+        <p>If a single integer is provided, it is used for 
<code>maxParameterCount</code>. If three integers are
+        provided, they are applied to <code>maxParameterCount</code>, 
<code>maxPartCount</code> and
+        <code>maxPartHeaderSize</code> respectively.
+        </p>
       </attribute>
 
     </attributes>


---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscr...@tomcat.apache.org
For additional commands, e-mail: dev-h...@tomcat.apache.org

Reply via email to