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

dsoumis pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 62b2511e62b4099b61920b1463e464476dee014e
Author: Dimitrios Soumis <jimsou...@gmail.com>
AuthorDate: Wed Jan 29 16:45:53 2025 +0200

    Add ParameterLimitValve to enforce request parameter limits for specific 
URLs (#753)
    
    Introduce ParameterLimitValve to enforce request parameter limits
    
    - Added `ParameterLimitValve`, a new valve that allows enforcing limits on 
the number of parameters in HTTP requests.
    - Supports defining per-URL pattern parameter limits using regular 
expressions.
    - Requests exceeding the configured limits are rejected with an HTTP 400 
Bad Request error.
    - Configuration is possible through `context.xml` and `server.xml`, or via 
dynamic management.
    - Includes integration tests (`TestParameterLimitValve`) to validate 
enforcement behavior across different contexts.
    - Added documentation detailing the valve’s attributes, configuration 
options, and usage examples.
    
    (cherry picked from commit ff49f19252aaf862faa62a624f6ffe224d76493d)
---
 java/org/apache/catalina/connector/Request.java    |  23 +-
 .../apache/catalina/valves/LocalStrings.properties |   6 +
 .../catalina/valves/ParameterLimitValve.java       | 265 +++++++++++++
 .../catalina/session/TestPersistentManager.java    |   2 +-
 .../catalina/valves/TestParameterLimitValve.java   | 428 +++++++++++++++++++++
 test/org/apache/catalina/valves/TestSSLValve.java  |  70 +++-
 webapps/docs/changelog.xml                         |   5 +
 webapps/docs/config/valve.xml                      |  43 +++
 8 files changed, 823 insertions(+), 19 deletions(-)

diff --git a/java/org/apache/catalina/connector/Request.java 
b/java/org/apache/catalina/connector/Request.java
index 85125223d6..0dfc63019d 100644
--- a/java/org/apache/catalina/connector/Request.java
+++ b/java/org/apache/catalina/connector/Request.java
@@ -145,6 +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();
+        }
         this.coyoteRequest = coyoteRequest;
         inputBuffer = new InputBuffer(coyoteRequest);
     }
@@ -411,6 +414,10 @@ public class Request implements HttpServletRequest {
 
     private HttpServletRequest applicationRequest = null;
 
+    /**
+     * The maximum number of request parameters
+     */
+    private int maxParameterCount = -1;
 
     // --------------------------------------------------------- Public Methods
 
@@ -441,6 +448,11 @@ public class Request implements HttpServletRequest {
         userPrincipal = null;
         subject = null;
         parametersParsed = false;
+        if (connector != null ) {
+            maxParameterCount = connector.getMaxParameterCount();
+        } else {
+            maxParameterCount = -1;
+        }
         if (parts != null) {
             for (Part part : parts) {
                 try {
@@ -829,6 +841,14 @@ public class Request implements HttpServletRequest {
         coyoteRequest.setServerPort(port);
     }
 
+    /**
+     * Set the maximum number of request parameters (GET plus POST) for a 
single request
+     *
+     * @param maxParameterCount The maximum number of request parameters
+     */
+    public void setMaxParameterCount(int maxParameterCount) {
+        this.maxParameterCount = maxParameterCount;
+    }
 
     // ------------------------------------------------- ServletRequest Methods
 
@@ -2429,7 +2449,6 @@ public class Request implements HttpServletRequest {
             }
         }
 
-        int maxParameterCount = getConnector().getMaxParameterCount();
         Parameters parameters = coyoteRequest.getParameters();
         parameters.setLimit(maxParameterCount);
 
@@ -2768,8 +2787,6 @@ public class Request implements HttpServletRequest {
 
         Parameters parameters = coyoteRequest.getParameters();
 
-        // Set this every time in case limit has been changed via JMX
-        int maxParameterCount = getConnector().getMaxParameterCount();
         if (parts != null && maxParameterCount > 0) {
             maxParameterCount -= parts.size();
         }
diff --git a/java/org/apache/catalina/valves/LocalStrings.properties 
b/java/org/apache/catalina/valves/LocalStrings.properties
index a78bfc810b..a2ae718737 100644
--- a/java/org/apache/catalina/valves/LocalStrings.properties
+++ b/java/org/apache/catalina/valves/LocalStrings.properties
@@ -172,3 +172,9 @@ stuckThreadDetectionValve.interrupted=Thread interrupted 
after the request is fi
 stuckThreadDetectionValve.notifyStuckThreadCompleted=Thread [{0}] (id=[{3}]) 
was previously reported to be stuck but has completed. It was active for 
approximately [{1}] milliseconds.{2,choice,0#|0< There is/are still [{2}] 
thread(s) that are monitored by this Valve and may be stuck.}
 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
new file mode 100644
index 0000000000..add411e3fd
--- /dev/null
+++ b/java/org/apache/catalina/valves/ParameterLimitValve.java
@@ -0,0 +1,265 @@
+/*
+ * 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 java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.regex.Pattern;
+
+import jakarta.servlet.ServletException;
+
+import org.apache.catalina.Container;
+import org.apache.catalina.Context;
+import org.apache.catalina.LifecycleException;
+import org.apache.catalina.connector.Request;
+import org.apache.catalina.connector.Response;
+import org.apache.juli.logging.LogFactory;
+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:
+ * <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>
+ * <li> Requires a <code>parameter_limit.config</code> file containing the 
URL-specific parameter limits.
+ * It must be placed in the Host configuration folder or in the WEB-INF folder 
of the web application.</li>
+ * </ul>
+ * <p>
+ * 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>:
+ * <pre>
+ * {@code
+ * <Context>
+ *     <Valve className="org.apache.catalina.valves.ParameterLimitValve"
+ * </Context>
+ * }
+ * and in <code>parameter_limit.config</code>:
+ * </pre>
+ * <pre>
+ * {@code
+ * /api/.*=150
+ * /admin/.*=50
+ * }
+ * </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
+ */
+
+public class ParameterLimitValve extends ValveBase {
+
+    /**
+     * Map for URL-specific limits
+     */
+    protected 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";
+
+    /**
+     * Will be set to true if the valve is associated with a context.
+     */
+    protected boolean context = false;
+
+    public ParameterLimitValve() {
+        super(true);
+    }
+
+    public String getResourcePath() {
+        return resourcePath;
+    }
+
+    public void setResourcePath(String resourcePath) {
+        this.resourcePath = resourcePath;
+    }
+
+    @Override
+    protected void initInternal() throws LifecycleException {
+        super.initInternal();
+        containerLog = LogFactory.getLog(getContainer().getLogName() + 
".parameterLimit");
+    }
+
+    @Override
+    protected void startInternal() throws LifecycleException {
+
+        super.startInternal();
+
+        InputStream is = null;
+
+        // Process configuration file for this valve
+        if (getContainer() instanceof Context) {
+            context = true;
+            String webInfResourcePath = "/WEB-INF/" + resourcePath;
+            is = ((Context) 
getContainer()).getServletContext().getResourceAsStream(webInfResourcePath);
+            if (containerLog.isDebugEnabled()) {
+                if (is == null) {
+                    
containerLog.debug(sm.getString("parameterLimitValve.noConfiguration", 
webInfResourcePath));
+                } else {
+                    
containerLog.debug(sm.getString("parameterLimitValve.readConfiguration", 
webInfResourcePath));
+                }
+            }
+        } else {
+            String resourceName = Container.getConfigPath(getContainer(), 
resourcePath);
+            try {
+                ConfigurationSource.Resource resource = 
ConfigFileLoader.getSource().getResource(resourceName);
+                is = resource.getInputStream();
+            } catch (IOException e) {
+                if (containerLog.isDebugEnabled()) {
+                    
containerLog.debug(sm.getString("parameterLimitValve.noConfiguration", 
resourceName), e);
+                }
+            }
+        }
+
+        if (is == null) {
+            // Will use management operations to configure the valve 
dynamically
+            return;
+        }
+
+        try (InputStreamReader isr = new InputStreamReader(is, 
StandardCharsets.UTF_8);
+             BufferedReader reader = new BufferedReader(isr)) {
+            setUrlPatternLimits(reader);
+        } catch (IOException ioe) {
+            containerLog.error(sm.getString("parameterLimitValve.closeError"), 
ioe);
+        } finally {
+            try {
+                is.close();
+            } catch (IOException e) {
+                
containerLog.error(sm.getString("parameterLimitValve.closeError"), e);
+            }
+        }
+
+    }
+
+    public void setUrlPatternLimits(String urlPatternConfig) {
+        urlPatternLimits.clear();
+        setUrlPatternLimits(new BufferedReader(new 
StringReader(urlPatternConfig)));
+    }
+
+    /**
+     * Set the mapping of URL patterns to their corresponding parameter limits.
+     * The input should be provided line by line, where each line contains a 
pattern and a limit, separated by the last '='.
+     * <p>
+     * Example:
+     * <pre>
+     * /api/.*=50
+     * /api======/.*=150
+     * /urlEncoded%20api=2
+     * # This is a comment
+     * </pre>
+     *
+     * @param reader A BufferedReader containing URL pattern to parameter 
limit mappings, with each pair on a separate line.
+     */
+    public void setUrlPatternLimits(BufferedReader reader) {
+        if (containerLog == null && getContainer() != null) {
+            containerLog = LogFactory.getLog(getContainer().getLogName() + 
".parameterLimit");
+        }
+        try {
+            String line;
+            while ((line = reader.readLine()) != null) {
+                // Trim whitespace from the line
+                line = line.trim();
+                if (line.isEmpty() || line.startsWith("#")) {
+                    // Skip empty lines or comments
+                    continue;
+                }
+
+                int lastEqualsIndex = line.lastIndexOf('=');
+                if (lastEqualsIndex == -1) {
+                    throw new 
IllegalArgumentException(sm.getString("parameterLimitValve.invalidLine", line));
+                }
+
+                String patternString = line.substring(0, 
lastEqualsIndex).trim();
+                String limitString = line.substring(lastEqualsIndex + 
1).trim();
+
+                Pattern pattern = 
Pattern.compile(UDecoder.URLDecode(patternString, StandardCharsets.UTF_8));
+                int limit = Integer.parseInt(limitString);
+                if (containerLog != null && containerLog.isTraceEnabled()) {
+                    containerLog.trace("Add pattern " + pattern + " and limit 
" + limit);
+                }
+                urlPatternLimits.put(pattern, Integer.valueOf(limit));
+            }
+        } catch (IOException e) {
+            containerLog.error(sm.getString("parameterLimitValve.readError"), 
e);
+        }
+    }
+
+    @Override
+    protected void stopInternal() throws LifecycleException {
+        super.stopInternal();
+        urlPatternLimits.clear();
+    }
+
+    /**
+     * Checks if any of the defined patterns matches the URI of the request 
and if it does,
+     * enforces the corresponding parameter limit for the request. Then invoke 
the next Valve in the sequence.
+     *
+     * @param request  The servlet request to be processed
+     * @param response The servlet response to be created
+     *
+     * @exception IOException      if an input/output error occurs
+     * @exception ServletException if a servlet error occurs
+     */
+    @Override
+    public void invoke(Request request, Response response) throws IOException, 
ServletException {
+
+        if (urlPatternLimits == null || urlPatternLimits.isEmpty()) {
+            getNext().invoke(request, response);
+            return;
+        }
+
+        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);
+                break;
+            }
+        }
+
+        // Invoke the next valve to continue processing the request
+        getNext().invoke(request, response);
+    }
+
+}
diff --git a/test/org/apache/catalina/session/TestPersistentManager.java 
b/test/org/apache/catalina/session/TestPersistentManager.java
index d1a1dbf2ed..be95c26687 100644
--- a/test/org/apache/catalina/session/TestPersistentManager.java
+++ b/test/org/apache/catalina/session/TestPersistentManager.java
@@ -108,6 +108,7 @@ public class TestPersistentManager {
         context.setParent(host);
 
         Connector connector = EasyMock.createNiceMock(Connector.class);
+        EasyMock.replay(connector);
         Request req = new Request(connector, null) {
             @Override
             public Context getContext() {
@@ -116,7 +117,6 @@ public class TestPersistentManager {
         };
         req.setRequestedSessionId("invalidSession");
         HttpServletRequest request = new RequestFacade(req);
-        EasyMock.replay(connector);
         requestCachingSessionListener.request = request;
 
         manager.setContext(context);
diff --git a/test/org/apache/catalina/valves/TestParameterLimitValve.java 
b/test/org/apache/catalina/valves/TestParameterLimitValve.java
new file mode 100644
index 0000000000..fea4d644f6
--- /dev/null
+++ b/test/org/apache/catalina/valves/TestParameterLimitValve.java
@@ -0,0 +1,428 @@
+/*
+ * 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 java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.FileWriter;
+import java.io.PrintWriter;
+import java.nio.charset.StandardCharsets;
+
+import jakarta.servlet.ServletRequestParametersBaseTest;
+
+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.core.StandardContext;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.scan.StandardJarScanner;
+
+
+public class TestParameterLimitValve extends ServletRequestParametersBaseTest {
+
+    @Test
+    public void testSpecificUrlPatternLimit() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        parameterLimitValve.setUrlPatternLimits("/special/.*=2");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/special/endpoint", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() +
+                "/special/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() + 
"/special/endpoint?param1=value1&param2=value2",
+            new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+
+        byte[] body = ("POST / HTTP/1.1" + CRLF +
+            "Host: localhost:" + getPort() + CRLF +
+            "Connection: close" + CRLF +
+            "Transfer-Encoding: chunked" + CRLF +
+            "Content-Type: application/x-www-form-urlencoded" + CRLF +
+            CRLF +
+            "param1=value1&param2=value2&param3=value3" + 
CRLF).getBytes(StandardCharsets.UTF_8);
+
+        rc = postUrl(body,"http://localhost:"; + getPort() + 
"/special/endpoint",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(400, rc);
+
+        body = ("POST / HTTP/1.1" + CRLF +
+            "Host: localhost:" + getPort() + CRLF +
+            "Connection: close" + CRLF +
+            "Transfer-Encoding: chunked" + CRLF +
+            "Content-Type: application/x-www-form-urlencoded" + CRLF +
+            CRLF +
+            "param1=value1&param2=value2" + 
CRLF).getBytes(StandardCharsets.UTF_8);
+
+        rc = postUrl(body, "http://localhost:"; + getPort() + 
"/special/endpoint",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(200, rc);
+
+        body = ("POST / HTTP/1.1" + CRLF +
+            "Host: localhost:" + getPort() + CRLF +
+            "Connection: close" + CRLF +
+            "Transfer-Encoding: chunked" + CRLF +
+            "Content-Type: application/x-www-form-urlencoded" + CRLF +
+            CRLF +
+            "param1=value1&param2=value2" + 
CRLF).getBytes(StandardCharsets.UTF_8);
+
+        rc = postUrl(body, "http://localhost:"; + getPort() + 
"/special/endpoint?param3=value3",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(400, rc);
+
+        body = ("POST / HTTP/1.1" + CRLF +
+            "Host: localhost:" + getPort() + CRLF +
+            "Connection: close" + CRLF +
+            "Transfer-Encoding: chunked" + CRLF +
+            "Content-Type: application/x-www-form-urlencoded" + CRLF +
+            CRLF +
+            "param1=value1" + CRLF).getBytes(StandardCharsets.UTF_8);
+
+        rc = postUrl(body, "http://localhost:"; + getPort() + 
"/special/endpoint?param2=value2",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() + "/special/endpoint", new 
ByteChunk(), null);
+
+        Assert.assertEquals(200, rc);
+    }
+
+    @Test
+    public void testMultipleEqualsPatternLimit() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        parameterLimitValve.setUrlPatternLimits("/special====2");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/special===", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() +
+                "/special===?param1=value1&param2=value2",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(200, rc);
+    }
+
+    @Test
+    public void testEncodedUrlPatternLimit() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        parameterLimitValve.setUrlPatternLimits("/special%20endpoint=2");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/special endpoint", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() +
+                
"/special%20endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(400, rc);
+    }
+
+    @Test
+    public void testMultipleSpecificUrlPatternsLimit() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        tomcat.getConnector().setMaxParameterCount(2);
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        parameterLimitValve.setUrlPatternLimits("/special/.*=2" + CRLF + 
"/special2/.*=3" + CRLF + "/my/special/url1=1");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/special/endpoint", "snoop");
+        ctx.addServletMappingDecoded("/special2/endpoint", "snoop");
+        ctx.addServletMappingDecoded("/my/special/url1", "snoop");
+        ctx.addServletMappingDecoded("/my/special/url2", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() +
+                "/special/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                
"/special2/endpoint?param1=value1&param2=value2&param3=value3&param4=value4",
+            new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/my/special/url1?param1=value1&param2=value2",
+            new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/my/special/url2?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/special/endpoint?param1=value1&param2=value2",
+            new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/special2/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/my/special/url1?param1=value1",
+            new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/my/special/url2?param1=value1&param2=value2",
+            new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+    }
+
+    @Test
+    public void testNoMatchingPatternWithConnectorLimit() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        tomcat.getConnector().setMaxParameterCount(1);
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        parameterLimitValve.setUrlPatternLimits("/special/.*=2");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/other/endpoint", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() + 
"/other/endpoint?param1=value1&param2=value2",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(400, rc);
+    }
+
+    @Test
+    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("/api/.*=2");
+            writer.println("# Commented line");
+        }
+
+        Tomcat tomcat = getTomcatInstance();
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+
+        try (BufferedReader reader = new BufferedReader(new 
FileReader(configFile))) {
+            parameterLimitValve.setUrlPatternLimits(reader);
+        }
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/api/test", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() + 
"/api/test?param1=value1&param2=value2", new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() + 
"/api/test?param1=value1&param2=value2&param3=value3", new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+    }
+
+    @Test
+    public void testUrlPatternLimitsWithEmptyFile() throws Exception {
+        File configFile = File.createTempFile("parameter_limit", ".config");
+
+        Tomcat tomcat = getTomcatInstance();
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+
+        try (BufferedReader reader = new BufferedReader(new 
FileReader(configFile))) {
+            parameterLimitValve.setUrlPatternLimits(reader);
+        }
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/api/test", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() + 
"/api/test?param1=value1&param2=value2", new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+    }
+
+    @Test
+    public void testUrlPatternLimitsFromFileAndProperty() throws Exception {
+        File configFile = File.createTempFile("parameter_limit", ".config");
+        try (PrintWriter writer = new PrintWriter(new FileWriter(configFile))) 
{
+            writer.println("# Commented line");
+            writer.println("/api/.*=2");
+            writer.println("# Commented line");
+        }
+
+        Tomcat tomcat = getTomcatInstance();
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+
+        parameterLimitValve.setUrlPatternLimits("/admin/.*=2");
+
+        try (BufferedReader reader = new BufferedReader(new 
FileReader(configFile))) {
+            parameterLimitValve.setUrlPatternLimits(reader);
+        }
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/api/test", "snoop");
+        ctx.addServletMappingDecoded("/admin/test", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() + 
"/api/test?param1=value1&param2=value2", new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() + 
"/admin/test?param1=value1&param2=value2", new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() + 
"/api/test?param1=value1&param2=value2&param3=value3", new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() + 
"/admin/test?param1=value1&param2=value2&param3=value3", new ByteChunk(), null);
+        Assert.assertEquals(400, rc);
+    }
+
+    @Test
+    public void testServerUrlPatternLimit() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getParent().getPipeline().addValve(parameterLimitValve);
+        parameterLimitValve.setUrlPatternLimits("/.*=2");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMappingDecoded("/special/endpoint", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() +
+                "/special/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/special/endpoint?param1=value1&param2=value2",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(200, rc);
+    }
+
+    @Test
+    public void testServerAndContextUrlPatternLimit() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        Context ctx1 = tomcat.addContext("context1", null);
+        ((StandardJarScanner) ctx1.getJarScanner()).setScanClassPath(false);
+
+        Context ctx2 = tomcat.addContext("context2", null);
+        ((StandardJarScanner) ctx2.getJarScanner()).setScanClassPath(false);
+
+        Context ctx3 = tomcat.addContext("context3", null);
+        ((StandardJarScanner) ctx2.getJarScanner()).setScanClassPath(false);
+
+        ParameterLimitValve serverParameterLimitValve = new 
ParameterLimitValve();
+        ParameterLimitValve contextParameterLimitValve = new 
ParameterLimitValve();
+        ParameterLimitValve context3ParameterLimitValve = new 
ParameterLimitValve();
+
+        ctx1.getParent().getPipeline().addValve(serverParameterLimitValve);
+
+        ctx1.getPipeline().addValve(contextParameterLimitValve);
+        ctx3.getPipeline().addValve(context3ParameterLimitValve);
+
+        serverParameterLimitValve.setUrlPatternLimits("/.*=2");
+        contextParameterLimitValve.setUrlPatternLimits("/special/.*=3");
+        context3ParameterLimitValve.setUrlPatternLimits("/special/.*=1");
+
+        Tomcat.addServlet(ctx1, "snoop", new SnoopServlet());
+        ctx1.addServletMappingDecoded("/special/endpoint", "snoop");
+
+        Tomcat.addServlet(ctx2, "snoop", new SnoopServlet());
+        ctx2.addServletMappingDecoded("/special/endpoint", "snoop");
+
+        Tomcat.addServlet(ctx3, "snoop", new SnoopServlet());
+        ctx3.addServletMappingDecoded("/special/endpoint", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() +
+                
"/context1/special/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(200, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                
"/context2/special/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(400, rc);
+
+        rc = getUrl("http://localhost:"; + getPort() +
+                "/context3/special/endpoint?param1=value1&param2=value2",
+            new ByteChunk(), null);
+
+        Assert.assertEquals(400, rc);
+    }
+
+}
diff --git a/test/org/apache/catalina/valves/TestSSLValve.java 
b/test/org/apache/catalina/valves/TestSSLValve.java
index 702606e5db..9a7993c9d2 100644
--- a/test/org/apache/catalina/valves/TestSSLValve.java
+++ b/test/org/apache/catalina/valves/TestSSLValve.java
@@ -21,7 +21,6 @@ import java.util.Arrays;
 import java.util.logging.Level;
 
 import org.junit.Assert;
-import org.junit.Before;
 import org.junit.Test;
 
 import org.apache.catalina.Globals;
@@ -36,8 +35,21 @@ public class TestSSLValve {
 
     public static class MockRequest extends Request {
 
-        public MockRequest() {
-            super(EasyMock.createMock(Connector.class), new 
org.apache.coyote.Request());
+        private static MockRequest single_instance = null;
+
+        public MockRequest(Connector connector) {
+            super(connector, new org.apache.coyote.Request());
+        }
+
+        public static MockRequest getInstance()
+        {
+            if (single_instance == null) {
+                Connector connector = EasyMock.createNiceMock(Connector.class);
+                EasyMock.replay(connector);
+                single_instance = new MockRequest(connector);
+            }
+
+            return single_instance;
         }
 
         @Override
@@ -90,22 +102,30 @@ public class TestSSLValve {
             "yoTBqEpJloWksrypqp3iL4PAL5+KkB2zp66+MVAg8LcEDFJggBBJCtv4SCWV7ZOB",
             "WLu8gep+XCwSn0Wb6D3eFs4DoIiMvQ6g2rS/pk7o5eWj", "-----END 
CERTIFICATE-----" };
 
-    private SSLValve valve = new SSLValve();
-
-    private MockRequest mockRequest = new MockRequest();
-    private Valve mockNext = EasyMock.createMock(Valve.class);
+    private final SSLValve valve = new SSLValve();
 
+    private MockRequest mockRequest;
+    private final Valve mockNext = EasyMock.createMock(Valve.class);
 
-    @Before
     public void setUp() throws Exception {
+        setUp(null);
+    }
+
+    public void setUp(Connector connector) throws Exception {
         valve.setNext(mockNext);
+        if (connector == null) {
+            mockRequest = MockRequest.getInstance();
+        } else {
+            EasyMock.replay(connector);
+            mockRequest = new MockRequest(connector);
+        }
         mockNext.invoke(mockRequest, null);
         EasyMock.replay(mockNext);
     }
 
-
     @Test
-    public void testSslHeader() {
+    public void testSslHeader() throws Exception {
+        setUp();
         final String headerName = "myheader";
         final String headerValue = "BASE64_HEADER_VALUE";
         mockRequest.setHeader(headerName, headerValue);
@@ -115,7 +135,8 @@ public class TestSSLValve {
 
 
     @Test
-    public void testSslHeaderNull() {
+    public void testSslHeaderNull() throws Exception {
+        setUp();
         final String headerName = "myheader";
         mockRequest.setHeader(headerName, null);
 
@@ -124,7 +145,8 @@ public class TestSSLValve {
 
 
     @Test
-    public void testSslHeaderNullModHeader() {
+    public void testSslHeaderNullModHeader() throws Exception {
+        setUp();
         final String headerName = "myheader";
         final String nullModHeaderValue = "(null)";
         mockRequest.setHeader(headerName, nullModHeaderValue);
@@ -135,12 +157,14 @@ public class TestSSLValve {
 
     @Test
     public void testSslHeaderNullName() throws Exception {
+        setUp();
         Assert.assertNull(valve.mygetHeader(mockRequest, null));
     }
 
 
     @Test
     public void testSslHeaderMultiples() throws Exception {
+        setUp();
         final String headerName = "myheader";
         final String headerValue = "BASE64_HEADER_VALUE";
         mockRequest.addHeader(headerName, headerValue);
@@ -152,6 +176,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertHeaderSingleSpace() throws Exception {
+        setUp();
         String singleSpaced = certificateSingleLine(" ");
         mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);
 
@@ -163,6 +188,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertHeaderMultiSpace() throws Exception {
+        setUp();
         String singleSpaced = certificateSingleLine("    ");
         mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);
 
@@ -174,6 +200,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertHeaderTab() throws Exception {
+        setUp();
         String singleSpaced = certificateSingleLine("\t");
         mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);
 
@@ -185,6 +212,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertHeaderEscaped() throws Exception {
+        setUp();
         String cert = certificateEscaped();
         mockRequest.setHeader(valve.getSslClientEscapedCertHeader(), cert);
 
@@ -196,6 +224,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertNull() throws Exception {
+        setUp();
         TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", 
null,
                 "org.apache.catalina.valves.SSLValve");
 
@@ -209,6 +238,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertShorter() throws Exception {
+        setUp();
         mockRequest.setHeader(valve.getSslClientCertHeader(), "shorter than 
hell");
 
         TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "", 
null,
@@ -224,6 +254,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertIgnoredBegin() throws Exception {
+        setUp();
         String[] linesBegin = Arrays.copyOf(CERTIFICATE_LINES, 
CERTIFICATE_LINES.length);
         linesBegin[0] = "3fisjcme3kdsakasdfsadkafsd3";
         String begin = certificateSingleLine(linesBegin, " ");
@@ -237,6 +268,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslClientCertBadFormat() throws Exception {
+        setUp();
         String[] linesDeleted = Arrays.copyOf(CERTIFICATE_LINES, 
CERTIFICATE_LINES.length / 2);
         String deleted = certificateSingleLine(linesDeleted, " ");
         mockRequest.setHeader(valve.getSslClientCertHeader(), deleted);
@@ -254,8 +286,10 @@ public class TestSSLValve {
 
     @Test
     public void testClientCertProviderNotFound() throws Exception {
-        
EasyMock.expect(mockRequest.getConnector().getProperty("clientCertProvider")).andStubReturn("wontBeFound");
-        EasyMock.replay(mockRequest.getConnector());
+        Connector connector = EasyMock.createNiceMock(Connector.class);
+        
EasyMock.expect(connector.getProperty("clientCertProvider")).andStubReturn("wontBeFound");
+        setUp(connector);
+
         mockRequest.setHeader(valve.getSslClientCertHeader(), 
certificateSingleLine(" "));
 
         TesterLogValidationFilter f = 
TesterLogValidationFilter.add(Level.SEVERE, null,
@@ -270,6 +304,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslSecureProtocolHeaderPresent() throws Exception {
+        setUp();
         String protocol = "secured-with";
         mockRequest.setHeader(valve.getSslSecureProtocolHeader(), protocol);
 
@@ -281,6 +316,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslCipherHeaderPresent() throws Exception {
+        setUp();
         String cipher = "ciphered-with";
         mockRequest.setHeader(valve.getSslCipherHeader(), cipher);
 
@@ -292,6 +328,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslSessionIdHeaderPresent() throws Exception {
+        setUp();
         String session = "ssl-session";
         mockRequest.setHeader(valve.getSslSessionIdHeader(), session);
 
@@ -303,6 +340,7 @@ public class TestSSLValve {
 
     @Test
     public void testSslCipherUserKeySizeHeaderPresent() throws Exception {
+        setUp();
         Integer keySize = Integer.valueOf(452);
         mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), 
String.valueOf(keySize));
 
@@ -314,12 +352,14 @@ public class TestSSLValve {
 
     @Test(expected = NumberFormatException.class)
     public void testSslCipherUserKeySizeHeaderBadFormat() throws Exception {
+        setUp();
         mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), 
"not-an-integer");
 
         try {
             valve.invoke(mockRequest, null);
         } catch (NumberFormatException e) {
             Assert.assertNull(mockRequest.getAttribute(Globals.KEY_SIZE_ATTR));
+            mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(), null);
             throw e;
         }
     }
@@ -363,4 +403,4 @@ public class TestSSLValve {
         Assert.assertNotNull(certificates[0]);
         Assert.assertEquals(0, f.getMessageCount());
     }
-}
\ No newline at end of file
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index dfb43d4431..b32dc1563f 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -184,6 +184,11 @@
       <fix>
         Enhance lifecycle of temporary files used by partial PUT. (remm)
       </fix>
+      <add>
+        Added support for limiting the number of parameters in HTTP requests 
through
+        the new <code>ParameterLimitValve</code>. The valve allows configurable
+        URL-specific limits on the number of parameters. (dsoumis)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Coyote">
diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml
index 3c945ccee7..da28586791 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -2672,6 +2672,49 @@
 
 </section>
 
+<section name="Parameter Limit Valve">
+
+  <subsection name="Introduction">
+
+    <p>The <strong>Parameter Limit Valve</strong> is used to limit the number 
of parameters allowed in HTTP requests
+    overriding the Connector's value. The valve can be configured with 
specific limits for certain URL patterns.
+    Requests exceeding the defined parameter limits will result in an HTTP 400 
Bad Request error.</p>
+
+  </subsection>
+
+  <subsection name="Attributes">
+
+    <p>The <strong>Parameter Limit Valve</strong> supports the following
+    configuration attributes:</p>
+
+    <attributes>
+
+      <attribute name="className" required="true">
+        <p>Java class name of the implementation to use.  This MUST be set to
+        <strong>org.apache.catalina.valves.ParameterLimitValve</strong>.</p>
+      </attribute>
+
+      <attribute name="resourcePath" required="false">
+        <p>A file consisting of line-separated URL patterns and their 
respective parameter limits.
+        Each entry should follow the format <code>urlPattern=limit</code>.
+        The valve will apply the limit defined for a URL pattern when a 
request matches that pattern.
+        If no pattern matches, the Connector's limit will be used.
+        For example:
+          <code>
+            /api/.*=100
+            /admin/.*=50
+          </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>
+      </attribute>
+
+    </attributes>
+
+  </subsection>
+
+</section>
+
 </body>
 
 


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


Reply via email to