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¶m2=value2¶m3=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¶m2=value2¶m3=value3", + new ByteChunk(), null); + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/other/endpoint?param1=value1¶m2=value2¶m3=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]
