CVE-2021-43797 (published 2021-12-09T19:15Z)

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 v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-444
cwe_name: Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
description: 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.
extended_description: 
	   HTTP requests or responses ("messages") can be
	   malformed or unexpected in ways that cause web servers or
	   clients to interpret the messages in different ways than
	   intermediary HTTP agents such as load balancers, reverse
	   proxies, web caching proxies, application firewalls,
	   etc. For example, an adversary may be able to add duplicate
	   or different header fields that a client or server might
	   interpret as one set of messages, whereas the intermediary
	   might interpret the same sequence of bytes as a different
	   set of messages. For example, discrepancies can arise in
	   how to handle duplicate headers like two Transfer-encoding
	   (TE) or two Content-length (CL), or the malicious HTTP
	   message will have different headers for TE and
	   CL.
	   The inconsistent parsing and interpretation of messages
	   can allow the adversary to "smuggle" a message to the
	   client/server without the intermediary being aware of it.
	   This weakness is usually the result of the usage
	   of outdated or incompatible HTTP protocol versions in the
	   HTTP agents.
         
         

Fix information:
repo_url: https://github.com/netty/netty
commit_details: repo_url: https://github.com/netty/netty
author: Norman Maurer
author_date: 2021-12-09 14:49:43+01:00
msg: 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
repository: repo_url: https://github.com/netty/netty
repo_name: netty/netty
description: Netty project - an event-driven asynchronous network application framework
repo_language: Java
owner: netty
file_changes: filename: DefaultHttpHeaders.java
change_type: ModificationType.MODIFY
diff: @@ -367,6 +367,10 @@ public class DefaultHttpHeaders extends HttpHeaders {
 
     private static void validateHeaderNameElement(byte value) {
         switch (value) {
+        case 0x1c:
+        case 0x1d:
+        case 0x1e:
+        case 0x1f:
         case 0x00:
         case '\t':
         case '\n':
@@ -391,6 +395,10 @@ public class DefaultHttpHeaders extends HttpHeaders {
 
     private static void validateHeaderNameElement(char value) {
         switch (value) {
+        case 0x1c:
+        case 0x1d:
+        case 0x1e:
+        case 0x1f:
         case 0x00:
         case '\t':
         case '\n':

programming_language: Java
filename: HttpObjectDecoder.java
change_type: ModificationType.MODIFY
diff: @@ -824,7 +824,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         int valueStart;
         int valueEnd;
 
-        nameStart = findNonWhitespace(sb, 0, false);
+        nameStart = findNonWhitespace(sb, 0);
         for (nameEnd = nameStart; nameEnd < length; nameEnd ++) {
             char ch = sb.charAtUnsafe(nameEnd);
             // https://tools.ietf.org/html/rfc7230#section-3.2.4
@@ -859,7 +859,7 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         }
 
         name = sb.subStringUnsafe(nameStart, nameEnd);
-        valueStart = findNonWhitespace(sb, colonEnd, true);
+        valueStart = findNonWhitespace(sb, colonEnd);
         if (valueStart == length) {
             value = EMPTY_VALUE;
         } else {
@@ -898,12 +898,12 @@ public abstract class HttpObjectDecoder extends ByteToMessageDecoder {
         return c == ' ' || c == (char) 0x09 || c == (char) 0x0B || c == (char) 0x0C || c == (char) 0x0D;
     }
 
-    private static int findNonWhitespace(AppendableCharSequence sb, int offset, boolean validateOWS) {
+    private static int findNonWhitespace(AppendableCharSequence sb, int offset) {
         for (int result = offset; result < sb.length(); ++result) {
             char c = sb.charAtUnsafe(result);
             if (!Character.isWhitespace(c)) {
                 return result;
-            } else if (validateOWS && !isOWS(c)) {
+            } else if (!isOWS(c)) {
                 // Only OWS is supported for whitespace
                 throw new IllegalArgumentException("Invalid separator, only a single space or horizontal tab allowed," +
                         " but received a '" + c + "' (0x" + Integer.toHexString(c) + ")");

programming_language: Java
filename: HttpRequestDecoderTest.java
change_type: ModificationType.MODIFY
diff: @@ -15,6 +15,7 @@
  */
 package io.netty.handler.codec.http;
 
+import io.netty.buffer.ByteBuf;
 import io.netty.buffer.Unpooled;
 import io.netty.channel.embedded.EmbeddedChannel;
 import io.netty.handler.codec.TooLongFrameException;
@@ -357,6 +358,75 @@ public class HttpRequestDecoderTest {
         assertFalse(channel.finish());
     }
 
+    @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);
+    }
+
     @Test
     public void testWhitespace() {
         String requestStr = "GET /some/path HTTP/1.1\r\n" +
@@ -366,9 +436,9 @@ public class HttpRequestDecoderTest {
     }
 
     @Test
-    public void testWhitespaceBeforeTransferEncoding01() {
+    public void testWhitespaceInTransferEncoding01() {
         String requestStr = "GET /some/path HTTP/1.1\r\n" +
-                " Transfer-Encoding : chunked\r\n" +
+                "Transfer-Encoding : chunked\r\n" +
                 "Content-Length: 1\r\n" +
                 "Host: netty.io\r\n\r\n" +
                 "a";
@@ -376,9 +446,9 @@ public class HttpRequestDecoderTest {
     }
 
     @Test
-    public void testWhitespaceBeforeTransferEncoding02() {
+    public void testWhitespaceInTransferEncoding02() {
         String requestStr = "POST / HTTP/1.1" +
-                " Transfer-Encoding : chunked\r\n" +
+                "Transfer-Encoding : chunked\r\n" +
                 "Host: target.com" +
                 "Content-Length: 65\r\n\r\n" +
                 "0\r\n\r\n" +
@@ -475,6 +545,7 @@ public class HttpRequestDecoderTest {
         assertTrue(request.headers().contains("Transfer-Encoding", "chunked", false));
         assertFalse(request.headers().contains("Content-Length"));
         LastHttpContent c = channel.readInbound();
+        c.release();
         assertFalse(channel.finish());
     }
 
@@ -499,11 +570,15 @@ public class HttpRequestDecoderTest {
     }
 
     private static void testInvalidHeaders0(String requestStr) {
+        testInvalidHeaders0(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII));
+    }
+
+    private static void testInvalidHeaders0(ByteBuf requestBuffer) {
         EmbeddedChannel channel = new EmbeddedChannel(new HttpRequestDecoder());
-        assertTrue(channel.writeInbound(Unpooled.copiedBuffer(requestStr, CharsetUtil.US_ASCII)));
+        assertTrue(channel.writeInbound(requestBuffer));
         HttpRequest request = channel.readInbound();
+        assertThat(request.decoderResult().cause(), instanceOf(IllegalArgumentException.class));
         assertTrue(request.decoderResult().isFailure());
-        assertTrue(request.decoderResult().cause() instanceof IllegalArgumentException);
         assertFalse(channel.finish());
     }
 }

programming_language: Java
filename: HttpResponseDecoderTest.java
change_type: ModificationType.MODIFY
diff: @@ -799,4 +799,82 @@ public class HttpResponseDecoderTest {
         c.release();
         assertFalse(channel.finish());
     }
+
+    @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());
+    }
 }

programming_language: Java