From be036109aa1ef61f33e7d69ef2e58be25f19188a Mon Sep 17 00:00:00 2001
From: HwangRock
Date: Sat, 27 Jun 2026 23:02:35 +0900
Subject: [PATCH] Add maxTrailerCount attribute for HTTP/1.1 chunked request
trailers
---
.../coyote/http11/AbstractHttp11Protocol.java | 25 +++
.../apache/coyote/http11/Http11Processor.java | 4 +-
.../http11/filters/ChunkedInputFilter.java | 15 +-
.../http11/filters/LocalStrings.properties | 1 +
.../filters/TestChunkedInputFilter.java | 155 ++++++++++++++++++
webapps/docs/changelog.xml | 9 +
webapps/docs/config/http.xml | 8 +
7 files changed, 213 insertions(+), 4 deletions(-)
diff --git a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
index ea71bf2eb161..ccfcbec8650d 100644
--- a/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
+++ b/java/org/apache/coyote/http11/AbstractHttp11Protocol.java
@@ -627,6 +627,31 @@ public void setMaxTrailerSize(int maxTrailerSize) {
}
+ /**
+ * Maximum number of trailing headers allowed in a chunked HTTP request.
+ */
+ private int maxTrailerCount = 100;
+
+ /**
+ * Get the maximum number of trailing headers allowed in a chunked HTTP request.
+ *
+ * @return The maximum number of trailing headers, or a value less than 0 for no limit
+ */
+ public int getMaxTrailerCount() {
+ return maxTrailerCount;
+ }
+
+ /**
+ * Set the maximum number of trailing headers allowed in a chunked HTTP request. Use a value of less than 0 for no
+ * limit.
+ *
+ * @param maxTrailerCount The maximum number of trailing headers
+ */
+ public void setMaxTrailerCount(int maxTrailerCount) {
+ this.maxTrailerCount = maxTrailerCount;
+ }
+
+
/**
* Maximum size of extension information in chunked encoding
*/
diff --git a/java/org/apache/coyote/http11/Http11Processor.java b/java/org/apache/coyote/http11/Http11Processor.java
index ede3b4af963d..6209340b67fa 100644
--- a/java/org/apache/coyote/http11/Http11Processor.java
+++ b/java/org/apache/coyote/http11/Http11Processor.java
@@ -180,8 +180,8 @@ public Http11Processor(AbstractHttp11Protocol> protocol, Adapter adapter) {
// Create and add the chunked filters.
inputBuffer.addFilter(new ChunkedInputFilter(request, protocol.getMaxTrailerSize(),
- protocol.getAllowedTrailerHeadersInternal(), protocol.getMaxExtensionSize(),
- protocol.getMaxSwallowSize()));
+ protocol.getMaxTrailerCount(), protocol.getAllowedTrailerHeadersInternal(),
+ protocol.getMaxExtensionSize(), protocol.getMaxSwallowSize()));
outputBuffer.addFilter(new ChunkedOutputFilter());
// Create and add the void filters.
diff --git a/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java
index eaa8fd3332b4..3aa24c36a0fd 100644
--- a/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java
+++ b/java/org/apache/coyote/http11/filters/ChunkedInputFilter.java
@@ -103,6 +103,7 @@ public class ChunkedInputFilter implements InputFilter, ApplicationBufferHandler
*/
private final long maxExtensionSize;
+ private final int maxTrailerCount;
private final int maxSwallowSize;
@@ -116,6 +117,7 @@ public class ChunkedInputFilter implements InputFilter, ApplicationBufferHandler
private volatile int chunkSizeDigitsRead = 0;
private volatile State extensionState = null;
private final AtomicLong extensionSize = new AtomicLong(0);
+ private volatile int trailerCount = 0;
private final HttpHeaderParser httpHeaderParser;
// ----------------------------------------------------------- Constructors
@@ -125,15 +127,17 @@ public class ChunkedInputFilter implements InputFilter, ApplicationBufferHandler
*
* @param request the HTTP request
* @param maxTrailerSize the maximum trailer size
+ * @param maxTrailerCount the maximum number of trailing headers, or a value less than 0 for no limit
* @param allowedTrailerHeaders the set of allowed trailer headers
* @param maxExtensionSize the maximum extension size
* @param maxSwallowSize the maximum swallow size
*/
- public ChunkedInputFilter(final Request request, int maxTrailerSize, Set allowedTrailerHeaders,
- int maxExtensionSize, int maxSwallowSize) {
+ public ChunkedInputFilter(final Request request, int maxTrailerSize, int maxTrailerCount,
+ Set allowedTrailerHeaders, int maxExtensionSize, int maxSwallowSize) {
this.request = request;
this.trailingHeaders = ByteBuffer.allocate(maxTrailerSize);
this.trailingHeaders.limit(0);
+ this.maxTrailerCount = maxTrailerCount;
this.allowedTrailerHeaders = allowedTrailerHeaders;
this.maxExtensionSize = maxExtensionSize;
this.maxSwallowSize = maxSwallowSize;
@@ -270,6 +274,7 @@ public void recycle() {
chunkSizeDigitsRead = 0;
extensionState = null;
extensionSize.set(0);
+ trailerCount = 0;
httpHeaderParser.recycle();
}
@@ -643,6 +648,12 @@ private boolean parseTrailerFields() throws IOException {
parseState = ParseState.ERROR;
throw new BadRequestException(iae);
}
+ if (status == HeaderParseStatus.HAVE_MORE_HEADERS) {
+ trailerCount++;
+ if (maxTrailerCount >= 0 && trailerCount > maxTrailerCount) {
+ throwBadRequestException(sm.getString("chunkedInputFilter.maxTrailerCount"));
+ }
+ }
} while (status == HeaderParseStatus.HAVE_MORE_HEADERS);
if (status == HeaderParseStatus.DONE) {
parseState = ParseState.FINISHED;
diff --git a/java/org/apache/coyote/http11/filters/LocalStrings.properties b/java/org/apache/coyote/http11/filters/LocalStrings.properties
index 3650316b88f9..977b3f38ef56 100644
--- a/java/org/apache/coyote/http11/filters/LocalStrings.properties
+++ b/java/org/apache/coyote/http11/filters/LocalStrings.properties
@@ -28,6 +28,7 @@ chunkedInputFilter.invalidTrailerHeaderName=Invalid trailer header name (non-tok
chunkedInputFilter.invalidTrailerHeaderValue=Invalid trailer header value (control character in value)
chunkedInputFilter.maxExtension=maxExtensionSize exceeded
chunkedInputFilter.maxTrailer=maxTrailerSize exceeded
+chunkedInputFilter.maxTrailerCount=maxTrailerCount exceeded
gzipOutputFilter.flushFail=Ignored exception while flushing gzip filter
diff --git a/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java
index 7d0ed6e67757..db6852ca598b 100644
--- a/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java
+++ b/test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java
@@ -47,6 +47,7 @@ public class TestChunkedInputFilter extends TomcatBaseTest {
private static final int EXT_SIZE_LIMIT = 10;
private static final int TRAILER_SIZE_LIMIT = 8 * 1024;
+ private static final int TRAILER_COUNT_LIMIT = 3;
@Test
public void testChunkHeaderCRLF() throws Exception {
@@ -280,6 +281,160 @@ public void testTrailingHeadersSizeLimitPipelining() throws Exception {
}
+ @Test
+ public void testTrailingHeadersCountLimitBelowLimit() throws Exception {
+ doTestTrailingHeadersCountLimit(TRAILER_COUNT_LIMIT - 1, true);
+ }
+
+
+ @Test
+ public void testTrailingHeadersCountLimitAtLimit() throws Exception {
+ doTestTrailingHeadersCountLimit(TRAILER_COUNT_LIMIT, true);
+ }
+
+
+ @Test
+ public void testTrailingHeadersCountLimitAboveLimit() throws Exception {
+ doTestTrailingHeadersCountLimit(TRAILER_COUNT_LIMIT + 1, false);
+ }
+
+
+ /*
+ * maxTrailerCount=-1 disables the count limit entirely; 50 trailers must be accepted.
+ * Total trailer bytes are ~850, well within the default maxTrailerSize (8 KiB).
+ */
+ @Test
+ public void testTrailingHeadersCountLimitDisabled() throws Exception {
+ doTestTrailingHeadersCountLimit(50, true, -1);
+ }
+
+
+ /*
+ * maxTrailerCount=0 means zero trailers are permitted; even one trailer must be rejected.
+ * Mirrors the HTTP/2 count=0 case in TestHttp2Limits.
+ */
+ @Test
+ public void testTrailingHeadersCountLimitZeroRejectsAny() throws Exception {
+ doTestTrailingHeadersCountLimit(1, false, 0);
+ }
+
+
+ /*
+ * maxTrailerCount=0 with no trailers sent: the limit guard (trailerCount > 0) is never
+ * triggered, so the request must succeed.
+ */
+ @Test
+ public void testTrailingHeadersCountLimitZeroAllowsNone() throws Exception {
+ doTestTrailingHeadersCountLimit(0, true, 0);
+ }
+
+
+ /*
+ * Positive parse assertion: proves trailers are actually echoed (not silently dropped)
+ * when the count is under the limit. EchoHeaderServlet outputs:
+ * {pre-read x-trailer1}{pre-read x-trailer2}{body-byte-count}
+ * {post-read x-trailer1}{post-read x-trailer2}
+ * Body "a=0&b=1" = 7 bytes; only x-trailer1 is sent and allowed.
+ */
+ @Test
+ public void testTrailingHeadersCountLimitParsedCorrectly() throws Exception {
+ Tomcat tomcat = getTomcatInstance();
+
+ Context ctx = getProgrammaticRootContext();
+
+ Assert.assertTrue(tomcat.getConnector().setProperty("maxTrailerCount", Integer.toString(TRAILER_COUNT_LIMIT)));
+ Assert.assertTrue(tomcat.getConnector().setProperty("allowedTrailerHeaders", "x-trailer1"));
+
+ Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(true));
+ ctx.addServletMappingDecoded("/", "servlet");
+
+ tomcat.start();
+
+ // @formatter:off
+ String[] request = new String[] {
+ "POST /echo-params.jsp HTTP/1.1" + CRLF +
+ "Host: any" + CRLF +
+ "Transfer-encoding: chunked" + CRLF +
+ SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
+ "Connection: close" + CRLF +
+ CRLF +
+ "3" + CRLF +
+ "a=0" + CRLF +
+ "4" + CRLF +
+ "&b=1" + CRLF +
+ "0" + CRLF +
+ "x-trailer1: TestValue1" + CRLF +
+ CRLF
+ };
+ // @formatter:on
+
+ TrailerClient client = new TrailerClient(tomcat.getConnector().getLocalPort());
+ client.setRequest(request);
+
+ client.connect();
+ client.processRequest();
+ Assert.assertTrue(client.isResponse200());
+ // pre-read trailers both null; 7 body bytes; post-read x-trailer1 echoed; x-trailer2 not sent -> null
+ Assert.assertEquals("nullnull7TestValue1null", client.getResponseBody());
+ }
+
+
+ private void doTestTrailingHeadersCountLimit(int trailerCount, boolean ok) throws Exception {
+ doTestTrailingHeadersCountLimit(trailerCount, ok, TRAILER_COUNT_LIMIT);
+ }
+
+
+ private void doTestTrailingHeadersCountLimit(int trailerCount, boolean ok, int maxTrailerCount) throws Exception {
+ // Setup Tomcat instance
+ Tomcat tomcat = getTomcatInstance();
+
+ // No file system docBase required
+ Context ctx = getProgrammaticRootContext();
+
+ Tomcat.addServlet(ctx, "servlet", new EchoHeaderServlet(false));
+ ctx.addServletMappingDecoded("/", "servlet");
+
+ // Limit the number of trailing headers
+ Assert.assertTrue(tomcat.getConnector().setProperty("maxTrailerCount", Integer.toString(maxTrailerCount)));
+ tomcat.start();
+
+ // Build a chunked request with trailerCount distinct trailer headers
+ StringBuilder trailerHeaders = new StringBuilder();
+ for (int i = 0; i < trailerCount; i++) {
+ trailerHeaders.append("x-trailer-").append(i).append(": a").append(CRLF);
+ }
+
+ // @formatter:off
+ String[] request = new String[] {
+ "POST /echo-params.jsp HTTP/1.1" + CRLF +
+ "Host: any" + CRLF +
+ "Transfer-encoding: chunked" + CRLF +
+ SimpleHttpClient.HTTP_HEADER_CONTENT_TYPE_FORM_URL_ENCODING +
+ "Connection: close" + CRLF +
+ CRLF +
+ "3" + CRLF +
+ "a=0" + CRLF +
+ "4" + CRLF +
+ "&b=1" + CRLF +
+ "0" + CRLF +
+ trailerHeaders.toString() +
+ CRLF
+ };
+ // @formatter:on
+
+ TrailerClient client = new TrailerClient(tomcat.getConnector().getLocalPort());
+ client.setRequest(request);
+
+ client.connect();
+ client.processRequest();
+ if (ok) {
+ Assert.assertTrue(client.getResponseLine(), client.isResponse200());
+ } else {
+ Assert.assertTrue(client.getResponseLine(), client.isResponse400());
+ }
+ }
+
+
/*
* Since limit includes CRLF at end of trailer and final CRLF
*/
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 75bfc3f0f3e5..5141a12d71c1 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -284,6 +284,15 @@
Remove support for HTTP 0.9. (markt)
+
+ Add a new maxTrailerCount attribute to the HTTP/1.1
+ connector that limits the number of trailing headers permitted in a
+ chunked request. The default of 100 is consistent with
+ maxHeaderCount and the equivalent attribute for HTTP/2.
+ Previously, trailing headers were limited only by
+ maxTrailerSize. A value less than 0 disables the limit.
+ (HwangRock)
+
Avoid a potential JVM crash if a suitable version of Tomcat Native is
diff --git a/webapps/docs/config/http.xml b/webapps/docs/config/http.xml
index 49dd080e088b..a54017678e5a 100644
--- a/webapps/docs/config/http.xml
+++ b/webapps/docs/config/http.xml
@@ -646,6 +646,14 @@
used.
+
+ Limits the number of trailing headers (also known as trailers) that
+ are permitted in a chunked HTTP request. If the request contains more
+ trailing headers than this limit, the request will be rejected. A value
+ of less than 0 means no limit. If not specified, this attribute is set
+ to 100.
+
+
The minimum number of threads always kept running. This includes both
active and idle threads. If not specified, the default of 10