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