CVE-2021-43797 (Java, published 2021-12-09T19:15Z)
Description: value: Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. Netty prior to version 4.1.71.Final skips control chars when they are present at the beginning / end of the header name. It should instead fail fast as these are not allowed by the spec and could lead to HTTP request smuggling. Failing to do the validation might cause netty to "sanitize" header names before it forward these to another remote system when used as proxy. This remote system can't see the invalid usage anymore, and therefore does not do the validation itself. Users should upgrade to version 4.1.71.Final.
Severity: MEDIUM (CVSS v3 base score: 6.5)
Weakness: CWE-444 \u2014 Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
The product acts as an intermediary HTTP agent
         (such as a proxy or firewall) in the data flow between two
         entities such as a client and server, but it does not
         interpret malformed HTTP requests or responses in ways that
         are consistent with how the messages will be processed by
         those entities that are at the ultimate destination.
Repository: https://github.com/netty/netty
Files changed:
codec-http/src/main/java/io/netty/handler/codec/http/DefaultHttpHeaders.java
codec-http/src/main/java/io/netty/handler/codec/http/HttpObjectDecoder.java
codec-http/src/test/java/io/netty/handler/codec/http/HttpRequestDecoderTest.java
codec-http/src/test/java/io/netty/handler/codec/http/HttpResponseDecoderTest.java
Commit message: Merge pull request from GHSA-wx5j-54mm-rqqq

Motivation:

We should validate that only OWS is allowed before / after a header name and otherwise throw. At the moment we just "strip" everything except OWS.

Modifications:

- Adjust code to correctly validate
- Add unit tests

Result:

More strict and correct behaviour

Vulnerable code:
        nameStart = findNonWhitespace(sb, 0, false);
        valueStart = findNonWhitespace(sb, colonEnd, true);
    private static int findNonWhitespace(AppendableCharSequence sb, int offset, boolean validateOWS) {
            } else if (validateOWS && !isOWS(c)) {
    public void testWhitespaceBeforeTransferEncoding01() {
                " Transfer-Encoding : chunked\r\n" +
    public void testWhitespaceBeforeTransferEncoding02() {
                " Transfer-Encoding : chunked\r\n" +
        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
        assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException);

Fixed code:
        case 0x1c:
        case 0x1d:
        case 0x1e:
        case 0x1f:
        case 0x1c:
        case 0x1d:
        case 0x1e:
        case 0x1f:
        nameStart = findNonWhitespace(sb, 0);
        valueStart = findNonWhitespace(sb, colonEnd);
    private static int findNonWhitespace(AppendableCharSequence sb, int offset) {
            } else if (!isOWS(c)) {
import io.netty.buffer.ByteBuf;
    @Test
    public void testHeaderNameStartsWithControlChar1c() {
        testHeaderNameStartsWithControlChar(0x1c);
    }

    @Test
    public void testHeaderNameStartsWithControlChar1d() {
        testHeaderNameStartsWithControlChar(0x1d);
    }

    @Test
    public void testHeaderNameStartsWithControlChar1e() {
        testHeaderNameStartsWithControlChar(0x1e);
    }

    @Test
    public void testHeaderNameStartsWithControlChar1f() {
        testHeaderNameStartsWithControlChar(0x1f);
    }

    @Test
    public void testHeaderNameStartsWithControlChar0c() {
        testHeaderNameStartsWithControlChar(0x0c);
    }

    private void testHeaderNameStartsWithControlChar(int controlChar) {
        ByteBuf requestBuffer = Unpooled.buffer();
        requestBuffer.writeCharSequence("GET /some/path HTTP/1.1\r\n" +
                "Host: netty.io\r\n", CharsetUtil.US_ASCII);
        requestBuffer.writeByte(controlChar);
        requestBuffer.writeCharSequence("Transfer-Encoding: chunked\r\n\r\n", CharsetUtil.US_ASCII);
        testInvalidHeaders0(requestBuffer);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1c() {
        testHeaderNameEndsWithControlChar(0x1c);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1d() {
        testHeaderNameEndsWithControlChar(0x1d);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1e() {
        testHeaderNameEndsWithControlChar(0x1e);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1f() {
        testHeaderNameEndsWithControlChar(0x1f);
    }

    @Test
    public void testHeaderNameEndsWithControlChar0c() {
        testHeaderNameEndsWithControlChar(0x0c);
    }

    private void testHeaderNameEndsWithControlChar(int controlChar) {
        ByteBuf requestBuffer = Unpooled.buffer();
        requestBuffer.writeCharSequence("GET /some/path HTTP/1.1\r\n" +
                "Host: netty.io\r\n", CharsetUtil.US_ASCII);
        requestBuffer.writeCharSequence("Transfer-Encoding", CharsetUtil.US_ASCII);
        requestBuffer.writeByte(controlChar);
        requestBuffer.writeCharSequence(": chunked\r\n\r\n", CharsetUtil.US_ASCII);
        testInvalidHeaders0(requestBuffer);
    }

    public void testWhitespaceInTransferEncoding01() {
                "Transfer-Encoding : chunked\r\n" +
    public void testWhitespaceInTransferEncoding02() {
                "Transfer-Encoding : chunked\r\n" +
        c.release();
        testInvalidHeaders0(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII));
    }

    private static void testInvalidHeaders0(ByteBuf requestBuffer) {
        assertTrue(channel.writeInbound(requestBuffer));
        assertThat(request.decoderResult().cause(), instanceOf(IllegalArgumentException.class));

    @Test
    public void testHeaderNameStartsWithControlChar1c() {
        testHeaderNameStartsWithControlChar(0x1c);
    }

    @Test
    public void testHeaderNameStartsWithControlChar1d() {
        testHeaderNameStartsWithControlChar(0x1d);
    }

    @Test
    public void testHeaderNameStartsWithControlChar1e() {
        testHeaderNameStartsWithControlChar(0x1e);
    }

    @Test
    public void testHeaderNameStartsWithControlChar1f() {
        testHeaderNameStartsWithControlChar(0x1f);
    }

    @Test
    public void testHeaderNameStartsWithControlChar0c() {
        testHeaderNameStartsWithControlChar(0x0c);
    }

    private void testHeaderNameStartsWithControlChar(int controlChar) {
        ByteBuf responseBuffer = Unpooled.buffer();
        responseBuffer.writeCharSequence("HTTP/1.1 200 OK\r\n" +
                "Host: netty.io\r\n", CharsetUtil.US_ASCII);
        responseBuffer.writeByte(controlChar);
        responseBuffer.writeCharSequence("Transfer-Encoding: chunked\r\n\r\n", CharsetUtil.US_ASCII);
        testInvalidHeaders0(responseBuffer);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1c() {
        testHeaderNameEndsWithControlChar(0x1c);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1d() {
        testHeaderNameEndsWithControlChar(0x1d);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1e() {
        testHeaderNameEndsWithControlChar(0x1e);
    }

    @Test
    public void testHeaderNameEndsWithControlChar1f() {
        testHeaderNameEndsWithControlChar(0x1f);
    }

    @Test
    public void testHeaderNameEndsWithControlChar0c() {
        testHeaderNameEndsWithControlChar(0x0c);
    }

    private void testHeaderNameEndsWithControlChar(int controlChar) {
        ByteBuf responseBuffer = Unpooled.buffer();
        responseBuffer.writeCharSequence("HTTP/1.1 200 OK\r\n" +
                "Host: netty.io\r\n", CharsetUtil.US_ASCII);
        responseBuffer.writeCharSequence("Transfer-Encoding", CharsetUtil.US_ASCII);
        responseBuffer.writeByte(controlChar);
        responseBuffer.writeCharSequence(": chunked\r\n\r\n", CharsetUtil.US_ASCII);
        testInvalidHeaders0(responseBuffer);
    }

    private static void testInvalidHeaders0(ByteBuf responseBuffer) {
        EmbeddedChannel channel = new EmbeddedChannel(new HttpResponseDecoder());
        assertTrue(channel.writeInbound(responseBuffer));
        HttpResponse response = channel.readInbound();
        assertThat(response.decoderResult().cause(), instanceOf(IllegalArgumentException.class));
        assertTrue(response.decoderResult().isFailure());
        assertFalse(channel.finish());
    }