This is an automated email from the ASF dual-hosted git repository. markt pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/11.0.x by this push: new 891ccdee0c Add support for new attributes to ParameterLimitValve 891ccdee0c is described below commit 891ccdee0c78294e1beb1e516c4e413510aeb7cd 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 | 7 ++ .../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(+), 32 deletions(-) diff --git a/java/org/apache/catalina/connector/Request.java b/java/org/apache/catalina/connector/Request.java index 9f2fd10a48..4100206667 100644 --- a/java/org/apache/catalina/connector/Request.java +++ b/java/org/apache/catalina/connector/Request.java @@ -147,7 +147,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); @@ -420,6 +422,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) { @@ -451,8 +457,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) { @@ -842,8 +852,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 */ @@ -851,6 +862,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 @SuppressWarnings("deprecation") @@ -2490,7 +2522,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. * @@ -2506,7 +2538,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 4f7bdfb213..fe058ae80b 100644 --- a/java/org/apache/catalina/valves/LocalStrings.properties +++ b/java/org/apache/catalina/valves/LocalStrings.properties @@ -149,6 +149,13 @@ parameterLimitValve.readError=Error reading configuration 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}] 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¶m2=value2¶m3=value3¶m4=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 b57d6bddda..57d13c59df 100644 --- a/webapps/docs/changelog.xml +++ b/webapps/docs/changelog.xml @@ -155,7 +155,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