Skip to content

Commit 8494b04

Browse files
authored
Verify we do not receive multiple content-length headers or a content-length and transfer-encoding: chunked header when using HTTP/1.1 (netty#9865)
Motivation: RFC7230 states that we should not accept multiple content-length headers and also should not accept a content-length header in combination with transfer-encoding: chunked Modifications: - Check for multiple content-length headers and if found mark message as invalid - Check if we found a content-length header and also a transfer-encoding: chunked and if so mark the message as invalid - Add unit test Result: Fixes netty#9861
1 parent 9a0ccf2 commit 8494b04

File tree

2 files changed

+99
-15
lines changed

2 files changed

+99
-15
lines changed

codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -600,23 +600,61 @@ private State readHeaders(ByteBuf buffer) {
600600
if (name != null) {
601601
headers.add(name, value);
602602
}
603+
603604
// reset name and value fields
604605
name = null;
605606
value = null;
606607

607-
State nextState;
608+
List<String> values = headers.getAll(HttpHeaderNames.CONTENT_LENGTH);
609+
int contentLengthValuesCount = values.size();
610+
611+
if (contentLengthValuesCount > 0) {
612+
// Guard against multiple Content-Length headers as stated in
613+
// https://tools.ietf.org/html/rfc7230#section-3.3.2:
614+
//
615+
// If a message is received that has multiple Content-Length header
616+
// fields with field-values consisting of the same decimal value, or a
617+
// single Content-Length header field with a field value containing a
618+
// list of identical decimal values (e.g., "Content-Length: 42, 42"),
619+
// indicating that duplicate Content-Length header fields have been
620+
// generated or combined by an upstream message processor, then the
621+
// recipient MUST either reject the message as invalid or replace the
622+
// duplicated field-values with a single valid Content-Length field
623+
// containing that decimal value prior to determining the message body
624+
// length or forwarding the message.
625+
if (contentLengthValuesCount > 1 && message.protocolVersion() == HttpVersion.HTTP_1_1) {
626+
throw new IllegalArgumentException("Multiple Content-Length headers found");
627+
}
628+
contentLength = Long.parseLong(values.get(0));
629+
}
608630

609631
if (isContentAlwaysEmpty(message)) {
610632
HttpUtil.setTransferEncodingChunked(message, false);
611-
nextState = State.SKIP_CONTROL_CHARS;
633+
return State.SKIP_CONTROL_CHARS;
612634
} else if (HttpUtil.isTransferEncodingChunked(message)) {
613-
nextState = State.READ_CHUNK_SIZE;
635+
// See https://tools.ietf.org/html/rfc7230#section-3.3.3
636+
//
637+
// If a message is received with both a Transfer-Encoding and a
638+
// Content-Length header field, the Transfer-Encoding overrides the
639+
// Content-Length. Such a message might indicate an attempt to
640+
// perform request smuggling (Section 9.5) or response splitting
641+
// (Section 9.4) and ought to be handled as an error. A sender MUST
642+
// remove the received Content-Length field prior to forwarding such
643+
// a message downstream.
644+
//
645+
// This is also what http_parser does:
646+
// https://github.com/nodejs/http-parser/blob/v2.9.2/http_parser.c#L1769
647+
if (contentLengthValuesCount > 0 && message.protocolVersion() == HttpVersion.HTTP_1_1) {
648+
throw new IllegalArgumentException(
649+
"Both 'Content-Length: " + contentLength + "' and 'Transfer-Encoding: chunked' found");
650+
}
651+
652+
return State.READ_CHUNK_SIZE;
614653
} else if (contentLength() >= 0) {
615-
nextState = State.READ_FIXED_LENGTH_CONTENT;
654+
return State.READ_FIXED_LENGTH_CONTENT;
616655
} else {
617-
nextState = State.READ_VARIABLE_LENGTH_CONTENT;
656+
return State.READ_VARIABLE_LENGTH_CONTENT;
618657
}
619-
return nextState;
620658
}
621659

622660
private long contentLength() {

codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java

Lines changed: 55 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -323,29 +323,75 @@ public void testTooLargeHeaders() {
323323

324324
@Test
325325
public void testWhitespace() {
326-
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
327326
String requestStr = "GET /some/path HTTP/1.1\r\n" +
328327
"Transfer-Encoding : chunked\r\n" +
329328
"Host: netty.io\n\r\n";
330-
331-
assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
332-
HttpRequest request = channel.readInbound();
333-
assertTrue(request.decoderResult().isFailure());
334-
assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException);
335-
assertFalse(channel.finish());
329+
testInvalidHeaders0(requestStr);
336330
}
337331

338332
@Test
339333
public void testHeaderWithNoValueAndMissingColon() {
340-
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
341334
String requestStr = "GET /some/path HTTP/1.1\r\n" +
342335
"Content-Length: 0\r\n" +
343336
"Host:\r\n" +
344337
"netty.io\r\n\r\n";
338+
testInvalidHeaders0(requestStr);
339+
}
340+
341+
@Test
342+
public void testMultipleContentLengthHeaders() {
343+
String requestStr = "GET /some/path HTTP/1.1\r\n" +
344+
"Content-Length: 1\r\n" +
345+
"Content-Length: 0\r\n\r\n" +
346+
"b";
347+
testInvalidHeaders0(requestStr);
348+
}
349+
350+
@Test
351+
public void testMultipleContentLengthHeaders2() {
352+
String requestStr = "GET /some/path HTTP/1.1\r\n" +
353+
"Content-Length: 1\r\n" +
354+
"Connection: close\r\n" +
355+
"Content-Length: 0\r\n\r\n" +
356+
"b";
357+
testInvalidHeaders0(requestStr);
358+
}
359+
360+
@Test
361+
public void testContentLengthHeaderWithCommaValue() {
362+
String requestStr = "GET /some/path HTTP/1.1\r\n" +
363+
"Content-Length: 1,1\r\n\r\n" +
364+
"b";
365+
testInvalidHeaders0(requestStr);
366+
}
345367

368+
@Test
369+
public void testMultipleContentLengthHeadersWithFolding() {
370+
String requestStr = "POST / HTTP/1.1\r\n" +
371+
"Host: example.com\r\n" +
372+
"Connection: close\r\n" +
373+
"Content-Length: 5\r\n" +
374+
"Content-Length:\r\n" +
375+
"\t6\r\n\r\n" +
376+
"123456";
377+
testInvalidHeaders0(requestStr);
378+
}
379+
380+
@Test
381+
public void testContentLengthHeaderAndChunked() {
382+
String requestStr = "POST / HTTP/1.1\r\n" +
383+
"Host: example.com\r\n" +
384+
"Connection: close\r\n" +
385+
"Content-Length: 5\r\n" +
386+
"Transfer-Encoding: chunked\r\n\r\n" +
387+
"0\r\n\r\n";
388+
testInvalidHeaders0(requestStr);
389+
}
390+
391+
private static void testInvalidHeaders0(String requestStr) {
392+
EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
346393
assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
347394
HttpRequest request = channel.readInbound();
348-
System.err.println(request.headers().names().toString());
349395
assertTrue(request.decoderResult().isFailure());
350396
assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException);
351397
assertFalse(channel.finish());

0 commit comments

Comments
 (0)