Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions java/org/apache/coyote/http11/AbstractHttp11Protocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
4 changes: 2 additions & 2 deletions java/org/apache/coyote/http11/Http11Processor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 13 additions & 2 deletions java/org/apache/coyote/http11/filters/ChunkedInputFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ public class ChunkedInputFilter implements InputFilter, ApplicationBufferHandler
*/
private final long maxExtensionSize;

private final int maxTrailerCount;

private final int maxSwallowSize;

Expand All @@ -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
Expand All @@ -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<String> allowedTrailerHeaders,
int maxExtensionSize, int maxSwallowSize) {
public ChunkedInputFilter(final Request request, int maxTrailerSize, int maxTrailerCount,
Set<String> 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;
Expand Down Expand Up @@ -270,6 +274,7 @@ public void recycle() {
chunkSizeDigitsRead = 0;
extensionState = null;
extensionSize.set(0);
trailerCount = 0;
httpHeaderParser.recycle();
}

Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
155 changes: 155 additions & 0 deletions test/org/apache/coyote/http11/filters/TestChunkedInputFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
*/
Expand Down
9 changes: 9 additions & 0 deletions webapps/docs/changelog.xml
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,15 @@
<update>
Remove support for HTTP 0.9. (markt)
</update>
<add>
Add a new <code>maxTrailerCount</code> attribute to the HTTP/1.1
connector that limits the number of trailing headers permitted in a
chunked request. The default of <code>100</code> is consistent with
<code>maxHeaderCount</code> and the equivalent attribute for HTTP/2.
Previously, trailing headers were limited only by
<code>maxTrailerSize</code>. A value less than 0 disables the limit.
(HwangRock)
</add>
<!-- Entries for backport and removal before 12.0.0-M1 below this line -->
<fix>
Avoid a potential JVM crash if a suitable version of Tomcat Native is
Expand Down
8 changes: 8 additions & 0 deletions webapps/docs/config/http.xml
Original file line number Diff line number Diff line change
Expand Up @@ -646,6 +646,14 @@
used.</p>
</attribute>

<attribute name="maxTrailerCount" required="false">
<p>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 <code>100</code>.</p>
</attribute>

<attribute name="minSpareThreads" required="false">
<p>The minimum number of threads always kept running. This includes both
active and idle threads. If not specified, the default of <code>10</code>
Expand Down