From d3fd9ac0380099de6bb1fb973234aa278000aecc Mon Sep 17 00:00:00 2001
From: Masakazu Kitajo <maskit@apache.org>
Date: Wed, 15 Jan 2025 11:10:36 -0700
Subject: [PATCH] Do not allow extra CRs in chunks (#11936) (#11942)

* Do not allow extra CRs in chunks (#11936)

* Do not allow extra CRs in chunks

* Renumber test uuid

* Add test cases and fix an oversight

* Use prefix increment

(cherry picked from commit f5f2256c00abbfd02c22fbae3937da1c7bd8a34f)

* Fix test case
---
 proxy/http/HttpTunnel.cc                      | 12 +++++
 .../bad_chunked_encoding.test.py              |  6 +--
 .../malformed_chunked_header.replay.yaml      | 49 +++++++++++++++++--
 3 files changed, 61 insertions(+), 6 deletions(-)

diff --git a/proxy/http/HttpTunnel.cc b/proxy/http/HttpTunnel.cc
index 4b20784f395..adb3cd9bc98 100644
--- a/proxy/http/HttpTunnel.cc
+++ b/proxy/http/HttpTunnel.cc
@@ -136,6 +136,7 @@ ChunkedHandler::read_size()
 {
   int64_t bytes_used;
   bool done = false;
+  int cr    = 0;
 
   while (chunked_reader->read_avail() > 0 && !done) {
     const char *tmp   = chunked_reader->start();
@@ -174,6 +175,9 @@ ChunkedHandler::read_size()
             done  = true;
             break;
           } else {
+            if (ParseRules::is_cr(*tmp)) {
+              ++cr;
+            }
             state = CHUNK_READ_SIZE_CRLF; // now look for CRLF
           }
         }
@@ -183,7 +187,15 @@ ChunkedHandler::read_size()
           cur_chunk_bytes_left = (cur_chunk_size = running_sum);
           state                = (running_sum == 0) ? CHUNK_READ_TRAILER_BLANK : CHUNK_READ_CHUNK;
           done                 = true;
+          cr                   = 0;
           break;
+        } else if (ParseRules::is_cr(*tmp)) {
+          if (cr != 0) {
+            state = CHUNK_READ_ERROR;
+            done  = true;
+            break;
+          }
+          ++cr;
         }
       } else if (state == CHUNK_READ_SIZE_START) {
         if (ParseRules::is_cr(*tmp)) {
diff --git a/tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py b/tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py
index e92181ccdf7..f22cb9d2d39 100644
--- a/tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py
+++ b/tests/gold_tests/chunked_encoding/bad_chunked_encoding.test.py
@@ -172,13 +172,13 @@ def runChunkedTraffic(self):
         # code from the verifier client.
         tr.Processes.Default.ReturnCode = 1
         tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
-            r"(Unexpected chunked content for key 4: too small|Failed HTTP/1 transaction with key: 4)",
+            r"(Unexpected chunked content for key 101: too small|Failed HTTP/1 transaction with key: 101)",
             "Verify that ATS closed the forth transaction.")
         tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
-            r"(Unexpected chunked content for key 5: too small|Failed HTTP/1 transaction with key: 5)",
+            r"(Unexpected chunked content for key 102: too small|Failed HTTP/1 transaction with key: 102)",
             "Verify that ATS closed the fifth transaction.")
         tr.Processes.Default.Streams.stdout += Testers.ContainsExpression(
-            r"(Unexpected chunked content for key 6: too small|Failed HTTP/1 transaction with key: 6)",
+            r"(Unexpected chunked content for key 103: too small|Failed HTTP/1 transaction with key: 103)",
             "Verify that ATS closed the sixth transaction.")
 
         # ATS should close the connection before any body gets through. "def"
diff --git a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
index ae135d77ab7..5f136a7eeba 100644
--- a/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
+++ b/tests/gold_tests/chunked_encoding/replays/malformed_chunked_header.replay.yaml
@@ -78,6 +78,26 @@ sessions:
     server-response:
       status: 200
 
+- transactions:
+  - client-request:
+      method: "POST"
+      version: "1.1"
+      url: /malformed/chunk/header3
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ Transfer-Encoding, chunked ]
+        - [ uuid, 4 ]
+      content:
+        transfer: plain
+        encoding: uri
+        # BWS cannot have CR
+        data: 3%0D%0D%0Aabc%0D%0A0%0D%0A%0D%0A
+
+    # The connection will be dropped and this response will not go out.
+    server-response:
+      status: 200
+
   #
   # Now repeat the above two malformed chunk header tests, but on the server
   # side.
@@ -90,7 +110,7 @@ sessions:
       headers:
         fields:
         - [ Host, example.com ]
-        - [ uuid, 4 ]
+        - [ uuid, 101 ]
 
     # The connection will be dropped and this response will not go out.
     server-response:
@@ -113,7 +133,7 @@ sessions:
       headers:
         fields:
         - [ Host, example.com ]
-        - [ uuid, 5 ]
+        - [ uuid, 102 ]
 
     # The connection will be dropped and this response will not go out.
     server-response:
@@ -136,7 +156,7 @@ sessions:
       headers:
         fields:
         - [ Host, example.com ]
-        - [ uuid, 6 ]
+        - [ uuid, 103 ]
 
     # The connection will be dropped and this response will not go out.
     server-response:
@@ -150,3 +170,26 @@ sessions:
         encoding: uri
         # Super large chunk header, larger than will fit in an int.
         data: 111111113%0D%0Adef%0D%0A0%0D%0A%0D%0A
+
+- transactions:
+  - client-request:
+      method: "GET"
+      version: "1.1"
+      url: /response/malformed/chunk/size2
+      headers:
+        fields:
+        - [ Host, example.com ]
+        - [ uuid, 104 ]
+
+    # The connection will be dropped and this response will not go out.
+    server-response:
+      status: 200
+      reason: OK
+      headers:
+        fields:
+        - [ Transfer-Encoding, chunked ]
+      content:
+        transfer: plain
+        encoding: uri
+        # BWS cannot have CR
+        data: 3%0D%0D%0Adef%0D%0A0%0D%0A%0D%0A
