This is an automated email from the ASF dual-hosted git repository.
markt pushed a commit to branch 9.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git
The following commit(s) were added to refs/heads/9.0.x by this push:
new 8954758204 Add ParameterLimitValve: enforce request parameter limit
per URL (#753)
8954758204 is described below
commit 89547582045de4a88edb8a723040c55c74623f69
Author: Dimitrios Soumis <[email protected]>
AuthorDate: Wed Jan 29 16:45:53 2025 +0200
Add ParameterLimitValve: enforce request parameter limit per URL (#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.
---
java/org/apache/catalina/connector/Request.java | 27 +-
.../apache/catalina/valves/LocalStrings.properties | 6 +
.../catalina/valves/ParameterLimitValve.java | 269 +++++++++++++
.../servlet/ServletRequestParametersBaseTest.java | 90 +++++
.../catalina/session/TestPersistentManager.java | 2 +-
.../catalina/valves/TestParameterLimitValve.java | 437 +++++++++++++++++++++
test/org/apache/catalina/valves/TestSSLValve.java | 68 +++-
webapps/docs/changelog.xml | 5 +
webapps/docs/config/valve.xml | 43 ++
9 files changed, 927 insertions(+), 20 deletions(-)
diff --git a/java/org/apache/catalina/connector/Request.java
b/java/org/apache/catalina/connector/Request.java
index 46da139173..c717371a5a 100644
--- a/java/org/apache/catalina/connector/Request.java
+++ b/java/org/apache/catalina/connector/Request.java
@@ -144,6 +144,10 @@ public class Request implements HttpServletRequest {
public Request(Connector connector) {
this.connector = connector;
+ if (connector != null) {
+ this.maxParameterCount = connector.getMaxParameterCount();
+ }
+
formats = new SimpleDateFormat[formatsTemplate.length];
for (int i = 0; i < formats.length; i++) {
formats[i] = (SimpleDateFormat) formatsTemplate[i].clone();
@@ -440,6 +444,10 @@ public class Request implements HttpServletRequest {
private HttpServletRequest applicationRequest = null;
+ /**
+ * The maximum number of request parameters
+ */
+ private int maxParameterCount = -1;
// --------------------------------------------------------- Public Methods
@@ -470,6 +478,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 {
@@ -857,6 +870,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
@@ -929,8 +950,8 @@ public class Request implements HttpServletRequest {
* {@inheritDoc}
* <p>
* The attribute names returned will only be those for the attributes set
via {@link #setAttribute(String, Object)}.
- * Tomcat internal attributes will not be included even though they are
accessible via {@link #getAttribute(String)}.
- * The Tomcat internal attributes include:
+ * Tomcat internal attributes will not be included even though they are
accessible via
+ * {@link #getAttribute(String)}. The Tomcat internal attributes include:
* <ul>
* <li>{@link Globals#DISPATCHER_TYPE_ATTR}</li>
* <li>{@link Globals#DISPATCHER_REQUEST_PATH_ATTR}</li>
@@ -2561,7 +2582,6 @@ public class Request implements HttpServletRequest {
}
}
- int maxParameterCount = getConnector().getMaxParameterCount();
Parameters parameters = coyoteRequest.getParameters();
parameters.setLimit(maxParameterCount);
@@ -2916,7 +2936,6 @@ public class Request implements HttpServletRequest {
boolean success = false;
try {
// 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 8cae3fbc36..fb62ba75a6 100644
--- a/java/org/apache/catalina/valves/LocalStrings.properties
+++ b/java/org/apache/catalina/valves/LocalStrings.properties
@@ -166,3 +166,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..0b511ffc1b
--- /dev/null
+++ b/java/org/apache/catalina/valves/ParameterLimitValve.java
@@ -0,0 +1,269 @@
+/*
+ * 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 javax.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/javax/servlet/ServletRequestParametersBaseTest.java
b/test/javax/servlet/ServletRequestParametersBaseTest.java
new file mode 100644
index 0000000000..17bd42ac46
--- /dev/null
+++ b/test/javax/servlet/ServletRequestParametersBaseTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 javax.servlet;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.catalina.startup.SimpleHttpClient;
+import org.apache.catalina.startup.TomcatBaseTest;
+
+public class ServletRequestParametersBaseTest extends TomcatBaseTest {
+
+ protected Map<String,List<String>>
parseReportedParameters(SimpleHttpClient client) {
+ Map<String,List<String>> parameters = new LinkedHashMap<>();
+ if (client.isResponse200()) {
+ // Response is written using "\n" so need to split on that.
+ String[] lines = client.getResponseBody().split("\n");
+ for (String line : lines) {
+ // Every line should be name=value
+ int equalsPos = line.indexOf('=');
+ String name = line.substring(0, equalsPos);
+ String value = line.substring(equalsPos + 1);
+
+ List<String> values = parameters.computeIfAbsent(name, k ->
new ArrayList<>());
+ values.add(value);
+ }
+ }
+ return parameters;
+ }
+
+
+ protected static class ParameterParsingServlet extends HttpServlet {
+
+ private static final long serialVersionUID = 1L;
+
+ @Override
+ protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
+
+ resp.setContentType("text/plain");
+ resp.setCharacterEncoding("UTF-8");
+ PrintWriter pw = resp.getWriter();
+
+ Enumeration<String> names = req.getParameterNames();
+ while (names.hasMoreElements()) {
+ String name = names.nextElement();
+ for (String value : req.getParameterValues(name)) {
+ pw.print(name + "=" + value + '\n');
+ }
+ }
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest req, HttpServletResponse
resp) throws ServletException, IOException {
+ // Required parameter processing is the same as for GET
+ doGet(req, resp);
+ }
+ }
+
+
+ protected static class TestParameterClient extends SimpleHttpClient {
+
+ @Override
+ public boolean isResponseBodyOK() {
+ return true;
+ }
+ }
+}
diff --git a/test/org/apache/catalina/session/TestPersistentManager.java
b/test/org/apache/catalina/session/TestPersistentManager.java
index 4aa69185d7..9d60256ca3 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) {
@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..a2e5bb5afc
--- /dev/null
+++ b/test/org/apache/catalina/valves/TestParameterLimitValve.java
@@ -0,0 +1,437 @@
+/*
+ * 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 javax.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.filters.FailedRequestFilter;
+import org.apache.catalina.startup.Tomcat;
+import org.apache.tomcat.util.buf.ByteChunk;
+import org.apache.tomcat.util.descriptor.web.FilterDef;
+import org.apache.tomcat.util.descriptor.web.FilterMap;
+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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx);
+
+ 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");
+
+ addFailedRequestFilter(ctx1);
+ addFailedRequestFilter(ctx2);
+ addFailedRequestFilter(ctx3);
+
+ 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);
+ }
+
+
+ private static void addFailedRequestFilter(Context context) {
+ FilterDef failedRequestFilter = new FilterDef();
+ failedRequestFilter.setFilterName("failedRequestFilter");
+
failedRequestFilter.setFilterClass(FailedRequestFilter.class.getName());
+ FilterMap failedRequestFilterMap = new FilterMap();
+ failedRequestFilterMap.setFilterName("failedRequestFilter");
+ failedRequestFilterMap.addURLPatternDecoded("/*");
+ context.addFilterDef(failedRequestFilter);
+ context.addFilterMap(failedRequestFilterMap);
+ }
+}
diff --git a/test/org/apache/catalina/valves/TestSSLValve.java
b/test/org/apache/catalina/valves/TestSSLValve.java
index 3d8df7bd01..ddd00e9736 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,11 +35,23 @@ public class TestSSLValve {
public static class MockRequest extends Request {
- public MockRequest() {
- super(EasyMock.createMock(Connector.class));
+ private static MockRequest single_instance = null;
+
+ public MockRequest(Connector connector) {
+ super(connector);
setCoyoteRequest(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
public void setAttribute(String name, Object value) {
getCoyoteRequest().getAttributes().put(name, value);
@@ -91,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);
@@ -116,7 +135,8 @@ public class TestSSLValve {
@Test
- public void testSslHeaderNull() {
+ public void testSslHeaderNull() throws Exception {
+ setUp();
final String headerName = "myheader";
mockRequest.setHeader(headerName, null);
@@ -125,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);
@@ -136,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);
@@ -153,6 +176,7 @@ public class TestSSLValve {
@Test
public void testSslClientCertHeaderSingleSpace() throws Exception {
+ setUp();
String singleSpaced = certificateSingleLine(" ");
mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);
@@ -164,6 +188,7 @@ public class TestSSLValve {
@Test
public void testSslClientCertHeaderMultiSpace() throws Exception {
+ setUp();
String singleSpaced = certificateSingleLine(" ");
mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);
@@ -175,6 +200,7 @@ public class TestSSLValve {
@Test
public void testSslClientCertHeaderTab() throws Exception {
+ setUp();
String singleSpaced = certificateSingleLine("\t");
mockRequest.setHeader(valve.getSslClientCertHeader(), singleSpaced);
@@ -186,6 +212,7 @@ public class TestSSLValve {
@Test
public void testSslClientCertHeaderEscaped() throws Exception {
+ setUp();
String cert = certificateEscaped();
mockRequest.setHeader(valve.getSslClientEscapedCertHeader(), cert);
@@ -197,6 +224,7 @@ public class TestSSLValve {
@Test
public void testSslClientCertNull() throws Exception {
+ setUp();
TesterLogValidationFilter f = TesterLogValidationFilter.add(null, "",
null,
"org.apache.catalina.valves.SSLValve");
@@ -210,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,
@@ -225,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, " ");
@@ -238,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);
@@ -255,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,
@@ -271,6 +304,7 @@ public class TestSSLValve {
@Test
public void testSslCipherHeaderPresent() throws Exception {
+ setUp();
String cipher = "ciphered-with";
mockRequest.setHeader(valve.getSslCipherHeader(), cipher);
@@ -282,6 +316,7 @@ public class TestSSLValve {
@Test
public void testSslSessionIdHeaderPresent() throws Exception {
+ setUp();
String session = "ssl-session";
mockRequest.setHeader(valve.getSslSessionIdHeader(), session);
@@ -293,6 +328,7 @@ public class TestSSLValve {
@Test
public void testSslCipherUserKeySizeHeaderPresent() throws Exception {
+ setUp();
Integer keySize = Integer.valueOf(452);
mockRequest.setHeader(valve.getSslCipherUserKeySizeHeader(),
String.valueOf(keySize));
@@ -304,12 +340,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;
}
}
@@ -353,4 +391,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 25ade009f2..05a0ca4fc6 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -116,6 +116,11 @@
in a single URL segment. Based on pull request <pr>860</pr> by Chenjp.
(markt)
</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 08ebb45c69..ad49c588c9 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -2678,6 +2678,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: [email protected]
For additional commands, e-mail: [email protected]