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

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

commit 92fe1871bf1781d4f0cb53c8755f090225af5534
Author: opencode <[email protected]>
AuthorDate: Mon Oct 12 12:03:10 2026 +0200

    Apply the first matching URL pattern in configuration order
    
    ParameterLimitValve stored its URL pattern limits in a ConcurrentHashMap
    keyed by java.util.regex.Pattern. Since Pattern does not override
    hashCode, the iteration order used to find the first matching pattern
    was based on the identity hashes of the compiled patterns: when several
    configured patterns matched a request, which limit won was arbitrary,
    unrelated to the order in which the patterns were configured, and could
    change between JVM restarts.
    
    Store the limits in a configuration ordered map. The map is no longer
    mutated after publication. Instead, the setters parse into a temporary
    LinkedHashMap and publish an unmodifiable snapshot by replacing the
    volatile field reference, which also removes the window in which
    concurrent requests could observe a cleared or partially populated
    limit map. Request processing takes a snapshot of the reference and
    applies the limits of the first matching pattern in configuration
    order. The synchronization of the configuration changes is now in place
    for the stop path as well.
    
    Document the ordering rule in the class javadoc and the configuration
    documentation, note the append and duplicate handling semantics of the
    BufferedReader setter, and add tests for both overlapping pattern order
    combinations.
---
 .../catalina/valves/ParameterLimitValve.java       | 67 ++++++++++++----------
 .../catalina/valves/TestParameterLimitValve.java   | 54 +++++++++++++++++
 webapps/docs/changelog.xml                         |  9 +++
 webapps/docs/config/valve.xml                      |  2 +
 4 files changed, 102 insertions(+), 30 deletions(-)

diff --git a/java/org/apache/catalina/valves/ParameterLimitValve.java 
b/java/org/apache/catalina/valves/ParameterLimitValve.java
index badcc58e40..c0b44d85f5 100644
--- a/java/org/apache/catalina/valves/ParameterLimitValve.java
+++ b/java/org/apache/catalina/valves/ParameterLimitValve.java
@@ -22,8 +22,9 @@ import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.StringReader;
 import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.LinkedHashMap;
 import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
 import java.util.regex.Pattern;
 
 import jakarta.servlet.ServletException;
@@ -54,8 +55,9 @@ import org.apache.tomcat.util.file.ConfigurationSource;
  * matched. URL patterns and their corresponding limits can be configured via 
a regular expression mapping through the
  * <code>urlPatternLimits</code> attribute.
  * <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.
+ * The Valve checks each incoming request and enforces the appropriate limit. 
If several patterns match a request, the
+ * first matching pattern in configuration order is applied. If a request 
exceeds the allowed number of parameters, a
+ * <code>400 Bad Request</code> response is returned.
  * <p>
  * Example, configuration in <code>context.xml</code>:
  *
@@ -88,9 +90,12 @@ import org.apache.tomcat.util.file.ConfigurationSource;
 public class ParameterLimitValve extends ValveBase {
 
     /**
-     * Map for URL-specific limits.
+     * Map for URL-specific limits. Entries are kept in configuration order so 
that, when several patterns match a
+     * request, the first configured matching pattern wins. The map is 
published by replacing the reference rather than
+     * by being modified after publication, so readers work with a snapshot of 
the reference and never observe a
+     * partially updated map.
      */
-    private Map<Pattern,Integer[]> urlPatternLimits = new 
ConcurrentHashMap<>();
+    private volatile Map<Pattern,Integer[]> urlPatternLimits = 
Collections.emptyMap();
 
     /**
      * Relative path to the configuration file. Note: If the valve's container 
is a context, this will be relative to
@@ -193,19 +198,19 @@ public class ParameterLimitValve extends ValveBase {
      *
      * @param urlPatternConfig The URL pattern to parameter limit mappings
      */
-    public void setUrlPatternLimits(String urlPatternConfig) {
-        Map<Pattern,Integer[]> newLimits =
-                parseUrlPatternLimits(new BufferedReader(new 
StringReader(urlPatternConfig)));
-        urlPatternLimits.clear();
-        urlPatternLimits.putAll(newLimits);
+    public synchronized void setUrlPatternLimits(String urlPatternConfig) {
+        urlPatternLimits = Collections.unmodifiableMap(
+                parseUrlPatternLimits(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 '='. The parsed limits are only
-     * applied if the complete input is parsed successfully. If parsing fails, 
the exception is propagated and any
-     * previously configured limits are left unchanged. If a read error 
occurs, it is logged and the input read so far
-     * is discarded without modifying the configured limits.
+     * line, where each line contains a pattern and a limit, separated by the 
last '='. The parsed limits are added
+     * after any previously configured limits. If a parsed pattern matches an 
already configured pattern, its limit
+     * replaces the previous one at the original position, preserving the 
configuration order used to select the
+     * matching pattern. The parsed limits are only applied if the complete 
input is parsed successfully. If parsing
+     * fails, the exception is propagated and any previously configured limits 
are left unchanged. If a read error
+     * occurs, it is logged and the input read so far is discarded without 
modifying the configured limits.
      * <p>
      * Example:
      *
@@ -219,8 +224,10 @@ public class ParameterLimitValve extends ValveBase {
      * @param reader A BufferedReader containing URL pattern to parameter 
limit mappings, with each pair on a separate
      *                   line.
      */
-    public void setUrlPatternLimits(BufferedReader reader) {
-        urlPatternLimits.putAll(parseUrlPatternLimits(reader));
+    public synchronized void setUrlPatternLimits(BufferedReader reader) {
+        Map<Pattern,Integer[]> newLimits = new 
LinkedHashMap<>(urlPatternLimits);
+        newLimits.putAll(parseUrlPatternLimits(reader));
+        urlPatternLimits = Collections.unmodifiableMap(newLimits);
     }
 
     /**
@@ -236,7 +243,7 @@ public class ParameterLimitValve extends ValveBase {
         if (containerLog == null && getContainer() != null) {
             containerLog = LogFactory.getLog(getContainer().getLogName() + 
".parameterLimit");
         }
-        Map<Pattern,Integer[]> newLimits = new ConcurrentHashMap<>();
+        Map<Pattern,Integer[]> newLimits = new LinkedHashMap<>();
         try {
             String line;
             while ((line = reader.readLine()) != null) {
@@ -281,33 +288,33 @@ public class ParameterLimitValve extends ValveBase {
     }
 
     @Override
-    protected void stopInternal() throws LifecycleException {
+    protected synchronized void stopInternal() throws LifecycleException {
         super.stopInternal();
-        urlPatternLimits.clear();
+        urlPatternLimits = Collections.emptyMap();
     }
 
     /**
-     * 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
+     * {@inheritDoc}
+     * <p>
+     * Checks if any of the defined patterns matches the URI of the request 
and if it does, enforces the parameter
+     * limit of the first matching pattern in configuration order. Then invoke 
the next Valve in the sequence.
      */
     @Override
     public void invoke(Request request, Response response) throws IOException, 
ServletException {
 
-        if (urlPatternLimits.isEmpty()) {
+        Map<Pattern,Integer[]> urlPatternLimitsSnapshot = urlPatternLimits;
+
+        if (urlPatternLimitsSnapshot.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()) 
{
+        // Iterate over the URL patterns and apply the limits of the first
+        // matching pattern. The map is in configuration order, so when several
+        // patterns match, the first configured matching pattern wins.
+        for (Map.Entry<Pattern,Integer[]> entry : 
urlPatternLimitsSnapshot.entrySet()) {
             if (entry.getKey().matcher(requestURI).matches()) {
                 Integer[] limits = entry.getValue();
                 // maxParameterCount should always be present
diff --git a/test/org/apache/catalina/valves/TestParameterLimitValve.java 
b/test/org/apache/catalina/valves/TestParameterLimitValve.java
index 6e98454e50..022cc9e151 100644
--- a/test/org/apache/catalina/valves/TestParameterLimitValve.java
+++ b/test/org/apache/catalina/valves/TestParameterLimitValve.java
@@ -582,6 +582,60 @@ public class TestParameterLimitValve extends 
TomcatBaseTest {
     }
 
 
+    @Test
+    public void testOverlappingPatternsFirstConfiguredWins() throws Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        // The general pattern is configured first, so its limit applies even
+        // to URIs that also match the more specific pattern configured after
+        parameterLimitValve.setUrlPatternLimits(
+                "/.*=100" + CRLF + "/special/.*=2");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMapping("/special/endpoint", "snoop");
+
+        tomcat.start();
+
+        int rc = getUrl("http://localhost:"; + getPort() +
+                "/special/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+    }
+
+    @Test
+    public void testOverlappingPatternsSpecificConfiguredFirstApplies() throws 
Exception {
+        Tomcat tomcat = getTomcatInstance();
+
+        StandardContext ctx = (StandardContext) getProgrammaticRootContext();
+
+        ParameterLimitValve parameterLimitValve = new ParameterLimitValve();
+        ctx.getPipeline().addValve(parameterLimitValve);
+        // The specific pattern is configured first, so its limit applies to
+        // matching URIs while the general pattern applies to the others
+        parameterLimitValve.setUrlPatternLimits(
+                "/special/.*=2" + CRLF + "/.*=100");
+
+        Tomcat.addServlet(ctx, "snoop", new SnoopServlet());
+        ctx.addServletMapping("/special/endpoint", "snoop");
+        ctx.addServletMapping("/other/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() +
+                "/other/endpoint?param1=value1&param2=value2&param3=value3",
+            new ByteChunk(), null);
+        Assert.assertEquals(200, rc);
+    }
+
     @Test
     public void testInvalidConfigLeavesLimitsUnchanged() throws Exception {
         Tomcat tomcat = getTomcatInstance();
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index f708c383b8..499b180d0b 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -1989,6 +1989,15 @@
         proxying to absolute HTTP and HTTPS URLs and applies connect and
         read timeouts to the connection to the proxy target. (remm)
       </fix>
+      <fix>
+        Make the selection of the effective parameter limits by
+        <code>ParameterLimitValve</code> deterministic when several configured
+        URL patterns match a request. Previously, the first matching pattern
+        was encountered in hash map iteration order, which is arbitrary for
+        <code>java.util.regex.Pattern</code> keys and could change between JVM
+        restarts. The limits are now stored in configuration order and the
+        first matching pattern in configuration order is applied. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Coyote">
diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml
index bcbc64ac77..04d55f6832 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -2913,6 +2913,8 @@ RequestHeader unset SSL_CLIENT_ESCAPED_CERT</source>
         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.
+        If several configured patterns match a request, the limit of the first
+        matching pattern in configuration order is applied.
         For example:
           <code>
             /api/.*=100


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to