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¶m2=value2¶m3=value3", + new ByteChunk(), null); + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + "/special/endpoint?param1=value1¶m2=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¶m2=value2¶m3=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¶m2=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¶m2=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¶m2=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¶m2=value2¶m3=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¶m2=value2¶m3=value3", + new ByteChunk(), null); + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/special2/endpoint?param1=value1¶m2=value2¶m3=value3¶m4=value4", + new ByteChunk(), null); + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/my/special/url1?param1=value1¶m2=value2", + new ByteChunk(), null); + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/my/special/url2?param1=value1¶m2=value2¶m3=value3", + new ByteChunk(), null); + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/special/endpoint?param1=value1¶m2=value2", + new ByteChunk(), null); + Assert.assertEquals(200, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/special2/endpoint?param1=value1¶m2=value2¶m3=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¶m2=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¶m2=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¶m2=value2", new ByteChunk(), null); + Assert.assertEquals(200, rc); + + rc = getUrl("http://localhost:" + getPort() + "/api/test?param1=value1¶m2=value2¶m3=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¶m2=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¶m2=value2", new ByteChunk(), null); + Assert.assertEquals(200, rc); + + rc = getUrl("http://localhost:" + getPort() + "/admin/test?param1=value1¶m2=value2", new ByteChunk(), null); + Assert.assertEquals(200, rc); + + rc = getUrl("http://localhost:" + getPort() + "/api/test?param1=value1¶m2=value2¶m3=value3", new ByteChunk(), null); + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + "/admin/test?param1=value1¶m2=value2¶m3=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¶m2=value2¶m3=value3", + new ByteChunk(), null); + + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/special/endpoint?param1=value1¶m2=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¶m2=value2¶m3=value3", + new ByteChunk(), null); + + Assert.assertEquals(200, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/context2/special/endpoint?param1=value1¶m2=value2¶m3=value3", + new ByteChunk(), null); + + Assert.assertEquals(400, rc); + + rc = getUrl("http://localhost:" + getPort() + + "/context3/special/endpoint?param1=value1¶m2=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