Repository: netty/netty
Issue: Non-proper handling of Content-Length and Transfer-Encoding: chunked headers

### Expected behavior
1.Only accept one Content-Length.RFC 7230 says `duplicate Content-Length header fields have been generated or combined by an upstream message processor, then the recipient MUST either reject the message as invalid or replace the duplicated field-values with a single valid Content-Length`.
2.Only accept identity and chunked Transport-Encoding
In this implementation, the order does not matter (it probably should). The Go implementation only uses the first value of the header.Seems to be in sync with the behaviour of AWS ALB. All other valid (gzip, compress, etc.) and invalid TE will return a 501, since we don't have readers for them I figured this was the right move, but feel free to correct me
### Actual behavior
1. But netty accept all.
2.Netty accpet random TE.
### Steps to reproduce
Use two CL to reproduce the first.
Use a chunked TE header and a random TE header.
Smiliar with [9571](https://github.com/netty/netty/issues/9571). It also cause http smuggling. Or see the other issue [benoitc/gunicorn#2176](https://github.com/benoitc/gunicorn/issues/2176) and the PR [benoitc/gunicorn#2181](https://github.com/benoitc/gunicorn/pull/2181)
### Minimal yet complete reproducer code (or URL to code)

### Netty version
all
### JVM version (e.g. `java -version`)

### OS version (e.g. `uname -a`)


Issue comments:
@ZeddYu I fixed 1) https://github.com/netty/netty/pull/9865 but I am not sure about 2) as netty is just a library so I think the user should verify the transfer-encoding as it also depends on what handlers are in the pipeline.
@normanmaurer But I test it with following code:
```java
            HttpHeaders headers = request.headers();
            if (!headers.isEmpty()) {
                for (Map.Entry<String, String> h: headers) {
                    CharSequence key = h.getKey();
                    CharSequence value = h.getValue();
                    buf.append("HEADER: ").append(key).append(" = ").append(value).append("\r\n");
                }
                buf.append("\r\n");
            }
```
Use this request headerto test:
```
Transfer-Encoding: chunked
Transfer-Encoding: some
```
Only get one TE header is 'some'.
And for the 1), why I use two CL header to test is still accepted?
@ZeddYu sorry I don't understand your question... Can you please try to rephrase ? As shown in the test-case of #9865 multiple `Content-Length` headers are not allowed anymore .
@normanmaurer All right. I paste an img.
![](https://zeddyuimg.oss-cn-shanghai.aliyuncs.com/20191210195522.png)
Using two CL header is still accepted. Netty version is 4.1.43 Final.
@ZeddYu sure as this is not merged yet and will be part of netty 4.1.44.Final. 
@normanmaurer All right. But it seems it has got another question.
![](https://zeddyuimg.oss-cn-shanghai.aliyuncs.com/20191210195815.png)
You see. It allows CRLF after the colon. But RFC7230 says only allow CRLF + 1*(SP/HTAB).
And for the 2) and this , I am sure it will cause problems.
Here it is, the obs-fold.
```
header-field   = field-name ":" OWS field-value OWS

     field-name     = token
     field-value    = *( field-content / obs-fold )
     field-content  = field-vchar [ 1*( SP / HTAB ) field-vchar ]
     field-vchar    = VCHAR / obs-text

     obs-fold       = CRLF 1*( SP / HTAB )
                    ; obsolete line folding
                    ; see Section 3.2.4
```
According to https://tools.ietf.org/html/rfc7230#section-3.3.2.
@ZeddYu If you think there is an other issue related to multi-line headers please open another issue. IHMO this is not related to this one. 
@normanmaurer What about the 2)?
@ZeddYu again I am not sure why we should do anything about 2) as what is supported depends on what the user is doing with the pipeline etc. Netty is just a library and not a server implementation itself. For example http_parser is not doing anything about 2) as well. 
@normanmaurer But just like 

> @normanmaurer But I test it with following code:
> 
> ```java
>             HttpHeaders headers = request.headers();
>             if (!headers.isEmpty()) {
>                 for (Map.Entry<String, String> h: headers) {
>                     CharSequence key = h.getKey();
>                     CharSequence value = h.getValue();
>                     buf.append("HEADER: ").append(key).append(" = ").append(value).append("\r\n");
>                 }
>                 buf.append("\r\n");
>             }
> ```
> 
> Use this request headerto test:
> 
> ```
> Transfer-Encoding: chunked
> Transfer-Encoding: some
> ```
> 
> Only get one TE header is 'some'.

@normanmaurer But just like what I said just now. I can't seem to get the first TE header. I use the code which is from  https://netty.io/4.1/xref/io/netty/example/http/snoop/HttpSnoopServer.html.


@ZeddYu please can you provide a few reproducer I can run... This back and forth just eats up a lot of time on both of our ends. 
Sorry for that.
Application.class:
```java
public class Application {

    public static void main(String[] args) throws Exception{
        HttpServer server = new HttpServer(8888);// 8081为启动端口
        server.start();
    }
}
```
HttpServer.class:
```java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

import java.net.InetSocketAddress;

/**
 * netty server
 * 2018/11/1.
 */
public class HttpServer {

    int port = 8888;

    public HttpServer(int port){
        this.port = port;
    }

    public void start() throws Exception{
        ServerBootstrap bootstrap = new ServerBootstrap();
        EventLoopGroup boss = new NioEventLoopGroup();
        EventLoopGroup work = new NioEventLoopGroup();
        bootstrap.group(boss,work)
                .handler(new LoggingHandler(LogLevel.DEBUG))
                .channel(NioServerSocketChannel.class)
                .childHandler(new HttpServerInitializer());

        ChannelFuture f = bootstrap.bind(new InetSocketAddress(port)).sync();
        System.out.println(" server start up on port : " + port);
        f.channel().closeFuture().sync();

    }

}
```
HttpServerInitialize.class
```java
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;

public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {

    @Override
    protected void initChannel(SocketChannel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast("httpAggregator",new HttpObjectAggregator(512*1024)); 
        pipeline.addLast(new HttpRequestHandler());

    }
}
```
HttpRequestHandler.class
```java
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.DecoderResult;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpContent;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpHeaders;
import io.netty.handler.codec.http.HttpObject;
import io.netty.handler.codec.http.HttpRequest;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.QueryStringDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.ServerCookieDecoder;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.util.CharsetUtil;

import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import static io.netty.handler.codec.http.HttpResponseStatus.*;
import static io.netty.handler.codec.http.HttpVersion.*;

public class HttpRequestHandler extends SimpleChannelInboundHandler<Object> {

    private HttpRequest request;
    /** Buffer that stores the response content */
    private final StringBuilder buf = new StringBuilder();

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        if (msg instanceof HttpRequest) {
            HttpRequest request = this.request = (HttpRequest) msg;

            if (HttpUtil.is100ContinueExpected(request)) {
                sendContinue(ctx);
            }

            buf.setLength(0);
            buf.append("WELCOME TO THE WILD WILD WEB SERVER\r\n");
            buf.append("===================================\r\n");

            buf.append("VERSION: ").append(request.protocolVersion()).append("\r\n");
            buf.append("HOSTNAME: ").append(request.headers().get(HttpHeaderNames.HOST, "unknown")).append("\r\n");
            buf.append("REQUEST_URI: ").append(request.uri()).append("\r\n\r\n");

            HttpHeaders headers = request.headers();
            if (!headers.isEmpty()) {
                for (Map.Entry<String, String> h: headers) {
                    CharSequence key = h.getKey();
                    CharSequence value = h.getValue();
                    buf.append("HEADER: ").append(key).append(" = ").append(value).append("\r\n");
                }
                buf.append("\r\n");
            }

            QueryStringDecoder queryStringDecoder = new QueryStringDecoder(request.uri());
            Map<String, List<String>> params = queryStringDecoder.parameters();
            if (!params.isEmpty()) {
                for (Entry<String, List<String>> p: params.entrySet()) {
                    String key = p.getKey();
                    List<String> vals = p.getValue();
                    for (String val : vals) {
                        buf.append("PARAM: ").append(key).append(" = ").append(val).append("\r\n");
                    }
                }
                buf.append("\r\n");
            }

            appendDecoderResult(buf, request);
        }

        if (msg instanceof HttpContent) {
            HttpContent httpContent = (HttpContent) msg;

            ByteBuf content = httpContent.content();
            if (content.isReadable()) {
                buf.append("CONTENT: ");
                buf.append(content.toString(CharsetUtil.UTF_8));
                buf.append("\r\n");
                appendDecoderResult(buf, request);
            }

            if (msg instanceof LastHttpContent) {
                buf.append("END OF CONTENT\r\n");

                LastHttpContent trailer = (LastHttpContent) msg;
                if (!trailer.trailingHeaders().isEmpty()) {
                    buf.append("\r\n");
                    for (CharSequence name: trailer.trailingHeaders().names()) {
                        for (CharSequence value: trailer.trailingHeaders().getAll(name)) {
                            buf.append("TRAILING HEADER: ");
                            buf.append(name).append(" = ").append(value).append("\r\n");
                        }
                    }
                    buf.append("\r\n");
                }

                if (!writeResponse(trailer, ctx)) {
                    // If keep-alive is off, close the connection once the content is fully written.
                    ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
                }
            }
        }
    }

    @Override
    protected void channelRead0(ChannelHandlerContext channelHandlerContext, Object o) throws Exception {

    }

    private static void appendDecoderResult(StringBuilder buf, HttpObject o) {
        DecoderResult result = o.decoderResult();
        if (result.isSuccess()) {
            return;
        }

        buf.append(".. WITH DECODER FAILURE: ");
        buf.append(result.cause());
        buf.append("\r\n");
    }

    private boolean writeResponse(HttpObject currentObj, ChannelHandlerContext ctx) {
        // Decide whether to close the connection or not.
        boolean keepAlive = HttpUtil.isKeepAlive(request);
        // Build the response object.
        FullHttpResponse response = new DefaultFullHttpResponse(
                HTTP_1_1, currentObj.decoderResult().isSuccess()? OK : BAD_REQUEST,
                Unpooled.copiedBuffer(buf.toString(), CharsetUtil.UTF_8));

        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-");

        if (keepAlive) {
            // Add 'Content-Length' header only for a keep-alive connection.
            response.headers().setInt(HttpHeaderNames.CONTENT_LENGTH, response.content().readableBytes());
            // Add keep alive header as per:
            // - http://www.w.org/Protocols/HTTP/./draft-ietf-http-v-spec-.html#Connection
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }

        // Encode the cookie.
        String cookieString = request.headers().get(HttpHeaderNames.COOKIE);
        if (cookieString != null) {
            Set<Cookie> cookies = ServerCookieDecoder.STRICT.decode(cookieString);
            if (!cookies.isEmpty()) {
                // Reset the cookies if necessary.
                for (Cookie cookie: cookies) {
                    response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
                }
            }
        } else {
            // Browser sent no cookie.  Add some.
            response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("key1", "value1"));
            response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode("key2", "value2"));
        }

        // Write the response.
        ctx.write(response);

        return keepAlive;
    }

    private static void sendContinue(ChannelHandlerContext ctx) {
        FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, CONTINUE, Unpooled.EMPTY_BUFFER);
        ctx.write(response);
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
        ctx.close();
    }
}
```
@normanmaurer And can I request a CVE for 1)?
Fix my description of 2).
I use this http request.
```http
POST / HTTP/1.1
Host:localhost
Connection: close
Content-Length: 1
Transfer-Encoding: chunked, something

0
```
And netty use CL header because of 'something' in TE header. And I get the '0' data successfully.
@ZeddYu yes please request a CVE for incorrectly handling Content-Length and Transfer-Encoding: chunked headers. 
It appears this was assigned a CVE ID
https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-7238
@joshbressers If I understand correctly, this was assigned CVE-2019-20445 (according to the CVE description)

https://nvd.nist.gov/vuln/detail/CVE-2019-20445

@normanmaurer Do you know if your pull request #9865 addresses both CVE-2019-20445 and CVE-2020-7238?
@artem-smotrakov yes
@normanmaurer Thanks for confirming that!

I just wrote a couple of tests based on https://github.com/jdordonezn/CVE-2020-72381/issues/1 which is mentioned in CVE-2020-7238. The test passed although I am not 100% sure if they are sufficient

```
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
index 1e780b7959..2548af0e2a 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
@@ -325,7 +325,30 @@ public class HttpRequestDecoderTest {
     public void testWhitespace() {
         String requestStr = "GET /some/path HTTP/1.1\r\n" +
                 "Transfer-Encoding : chunked\r\n" +
-                "Host: netty.io\n\r\n";
+                "Host: netty.io\r\n\r\n";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testWhitespaceBeforeTransferEncoding01() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                " Transfer-Encoding : chunked\r\n" +
+                "Content-Length: 1\r\n" +
+                "Host: netty.io\r\n\r\n" +
+                "a";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testWhitespaceBeforeTransferEncoding02() {
+        String requestStr = "POST / HTTP/1.1" +
+                " Transfer-Encoding : chunked\r\n" +
+                "Host: target.com" +
+                "Content-Length: 65\r\n\r\n" +
+                "0\r\n\r\n" +
+                "GET /maliciousRequest HTTP/1.1\r\n" +
+                "Host: evilServer.com\r\n" +
+                "Foo: x";
         testInvalidHeaders0(requestStr);
     }

```
@artem-smotrakov thanks... would you be willing to submit these tests as a PR so we can include them and ensure we not regress ?
@normanmaurer No problem, I'll open a pull request.
Is there any plan to backport this to 3.x? There are certain libraries still out there coupled to the 3.x package naming structure so if this fix isn't backported, they will be forever vulnerable.
@JLLeitschuh sorry but no backport will be done ... netty 3.x is EOL for many years and if projects did not update yet they may have a reason to do now.

Pull request review comments:
Should we check the RFC test case?
```
Content-Length: 42, 42
```
done
Maybe catch `NumberFormatException` ("Content-Length: 42, 42") and re throw with a meaningful message ?
I would prefer to just keep it as it is as it did throw the same type of exception before already 
OK, but it can be improved in Netty 5 ;)
I netty 5 I think we want to re-evulate the http stuff anyway /cc @Scottmitch 
nit: should this be done after the `if` check below to avoid parsing the `content-length` header if there are multiple such headers?:

```java
if (message.protocolVersion() == HttpVersion.HTTP_1_1) {
 .....
}
contentLength = Long.parseLong(values.get(0));
```
nit: Rename `size` to `contentLengthValuesCount` to disambiguate `size`.
```suggestion
                        "'Both Content-Length: " + contentLength + "' and 'Transfer-Encoding: chunked' found");
```
Looking at the code, this is not an issue but you may want to test non-contiguous `content-length` headers:

```java
String requestStr = "GET /some/path HTTP/1.1\r\n" +
                 "Content-Length: 1\r\n" +
                 "Connection: close\r\n" +
                 "Content-Length: 0\r\n\r\n" +
                 "b";


Patch that resolved the issue:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java
index 28f048252fe..768bd3b26f5 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java
@@ -600,23 +600,61 @@ private State readHeaders(ByteBuf buffer) {
         if (name != null) {
             headers.add(name, value);
         }
+
         // reset name and value fields
         name = null;
         value = null;
 
-        State nextState;
+        List<String> values = headers.getAll(HttpHeaderNames.CONTENT_LENGTH);
+        int contentLengthValuesCount = values.size();
+
+        if (contentLengthValuesCount > 0) {
+            // Guard against multiple Content-Length headers as stated in
+            // https://tools.ietf.org/html/rfc7230#section-3.3.2:
+            //
+            // If a message is received that has multiple Content-Length header
+            //   fields with field-values consisting of the same decimal value, or a
+            //   single Content-Length header field with a field value containing a
+            //   list of identical decimal values (e.g., "Content-Length: 42, 42"),
+            //   indicating that duplicate Content-Length header fields have been
+            //   generated or combined by an upstream message processor, then the
+            //   recipient MUST either reject the message as invalid or replace the
+            //   duplicated field-values with a single valid Content-Length field
+            //   containing that decimal value prior to determining the message body
+            //   length or forwarding the message.
+            if (contentLengthValuesCount > 1 && message.protocolVersion() == HttpVersion.HTTP_1_1) {
+                throw new IllegalArgumentException("Multiple Content-Length headers found");
+            }
+            contentLength = Long.parseLong(values.get(0));
+        }
 
         if (isContentAlwaysEmpty(message)) {
             HttpUtil.setTransferEncodingChunked(message, false);
-            nextState = State.SKIP_CONTROL_CHARS;
+            return State.SKIP_CONTROL_CHARS;
         } else if (HttpUtil.isTransferEncodingChunked(message)) {
-            nextState = State.READ_CHUNK_SIZE;
+            // See https://tools.ietf.org/html/rfc7230#section-3.3.3
+            //
+            //       If a message is received with both a Transfer-Encoding and a
+            //       Content-Length header field, the Transfer-Encoding overrides the
+            //       Content-Length.  Such a message might indicate an attempt to
+            //       perform request smuggling (Section 9.5) or response splitting
+            //       (Section 9.4) and ought to be handled as an error.  A sender MUST
+            //       remove the received Content-Length field prior to forwarding such
+            //       a message downstream.
+            //
+            // This is also what http_parser does:
+            // https://github.com/nodejs/http-parser/blob/v2.9.2/http_parser.c#L1769
+            if (contentLengthValuesCount > 0 && message.protocolVersion() == HttpVersion.HTTP_1_1) {
+                throw new IllegalArgumentException(
+                        "Both 'Content-Length: " + contentLength + "' and 'Transfer-Encoding: chunked' found");
+            }
+
+            return State.READ_CHUNK_SIZE;
         } else if (contentLength() >= 0) {
-            nextState = State.READ_FIXED_LENGTH_CONTENT;
+            return State.READ_FIXED_LENGTH_CONTENT;
         } else {
-            nextState = State.READ_VARIABLE_LENGTH_CONTENT;
+            return State.READ_VARIABLE_LENGTH_CONTENT;
         }
-        return nextState;
     }
 
     private long contentLength() {


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
index 8a2345837fe..1e780b7959f 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
@@ -323,29 +323,75 @@ public void testTooLargeHeaders() {
 
     @Test
     public void testWhitespace() {
-        EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
         String requestStr = "GET /some/path HTTP/1.1\r\n" +
                 "Transfer-Encoding : chunked\r\n" +
                 "Host: netty.io\n\r\n";
-
-        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
-        HttpRequest request = channel.readInbound();
-        assertTrue(request.decoderResult().isFailure());
-        assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException);
-        assertFalse(channel.finish());
+        testInvalidHeaders0(requestStr);
     }
 
     @Test
     public void testHeaderWithNoValueAndMissingColon() {
-        EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
         String requestStr = "GET /some/path HTTP/1.1\r\n" +
                 "Content-Length: 0\r\n" +
                 "Host:\r\n" +
                 "netty.io\r\n\r\n";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testMultipleContentLengthHeaders() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Content-Length: 1\r\n" +
+                "Content-Length: 0\r\n\r\n" +
+                "b";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testMultipleContentLengthHeaders2() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Content-Length: 1\r\n" +
+                "Connection: close\r\n" +
+                "Content-Length: 0\r\n\r\n" +
+                "b";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testContentLengthHeaderWithCommaValue() {
+        String requestStr = "GET /some/path HTTP/1.1\r\n" +
+                "Content-Length: 1,1\r\n\r\n" +
+                "b";
+        testInvalidHeaders0(requestStr);
+    }
 
+    @Test
+    public void testMultipleContentLengthHeadersWithFolding() {
+        String requestStr = "POST / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Connection: close\r\n" +
+                "Content-Length: 5\r\n" +
+                "Content-Length:\r\n" +
+                "\t6\r\n\r\n" +
+                "123456";
+        testInvalidHeaders0(requestStr);
+    }
+
+    @Test
+    public void testContentLengthHeaderAndChunked() {
+        String requestStr = "POST / HTTP/1.1\r\n" +
+                "Host: example.com\r\n" +
+                "Connection: close\r\n" +
+                "Content-Length: 5\r\n" +
+                "Transfer-Encoding: chunked\r\n\r\n" +
+                "0\r\n\r\n";
+        testInvalidHeaders0(requestStr);
+    }
+
+    private static void testInvalidHeaders0(String requestStr) {
+        EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
         assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
         HttpRequest request = channel.readInbound();
-        System.err.println(request.headers().names().toString());
         assertTrue(request.decoderResult().isFailure());
         assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException);
         assertFalse(channel.finish());