Add validation field for chunk extension validation.

PiperOrigin-RevId: 620832541
diff --git a/quiche/balsa/balsa_enums.cc b/quiche/balsa/balsa_enums.cc
index bc6b68f..fa072fc 100644
--- a/quiche/balsa/balsa_enums.cc
+++ b/quiche/balsa/balsa_enums.cc
@@ -76,6 +76,8 @@
       return "INVALID_CHUNK_LENGTH";
     case CHUNK_LENGTH_OVERFLOW:
       return "CHUNK_LENGTH_OVERFLOW";
+    case INVALID_CHUNK_EXTENSION:
+      return "INVALID_CHUNK_EXTENSION";
     case CALLED_BYTES_SPLICED_WHEN_UNSAFE_TO_DO_SO:
       return "CALLED_BYTES_SPLICED_WHEN_UNSAFE_TO_DO_SO";
     case CALLED_BYTES_SPLICED_AND_EXCEEDED_SAFE_SPLICE_AMOUNT:
diff --git a/quiche/balsa/balsa_enums.h b/quiche/balsa/balsa_enums.h
index 60537a3..61c487e 100644
--- a/quiche/balsa/balsa_enums.h
+++ b/quiche/balsa/balsa_enums.h
@@ -81,6 +81,7 @@
     // Chunking errors
     INVALID_CHUNK_LENGTH,
     CHUNK_LENGTH_OVERFLOW,
+    INVALID_CHUNK_EXTENSION,
 
     // Other errors.
     CALLED_BYTES_SPLICED_WHEN_UNSAFE_TO_DO_SO,
diff --git a/quiche/balsa/balsa_frame.cc b/quiche/balsa/balsa_frame.cc
index 03bc994..1e6080f 100644
--- a/quiche/balsa/balsa_frame.cc
+++ b/quiche/balsa/balsa_frame.cc
@@ -1129,6 +1129,12 @@
             return current - input;
           }
           const char c = *current;
+          if (http_validation_policy_.disallow_lone_cr_in_chunk_extension &&
+              c == '\r' && (current + 1 == end || *(current + 1) != '\n')) {
+            // We have a lone carriage return.
+            HandleError(BalsaFrameEnums::INVALID_CHUNK_EXTENSION);
+            return current - input;
+          }
           if (c == '\r' || c == '\n') {
             extensions_length = (extensions_start == current)
                                     ? 0
diff --git a/quiche/balsa/balsa_frame_test.cc b/quiche/balsa/balsa_frame_test.cc
index 294e11f..e495bc0 100644
--- a/quiche/balsa/balsa_frame_test.cc
+++ b/quiche/balsa/balsa_frame_test.cc
@@ -1667,6 +1667,29 @@
   EXPECT_EQ(BalsaFrameEnums::BALSA_NO_ERROR, balsa_frame_.ErrorCode());
 }
 
+TEST_F(HTTPBalsaFrameTest, InvalidChunkExtensionWithCarriageReturn) {
+  balsa_frame_.set_http_validation_policy(
+      HttpValidationPolicy{.disallow_lone_cr_in_chunk_extension = true});
+  std::string message_headers =
+      "POST /potato?salad=withmayo HTTP/1.1\r\n"
+      "transfer-encoding: chunked\r\n"
+      "\r\n";
+  std::string message_body =
+      "9; bad\rextension\r\n"
+      "012345678\r\n"
+      "0\r\n"
+      "\r\n";
+  std::string message =
+      std::string(message_headers) + std::string(message_body);
+
+  EXPECT_CALL(visitor_mock_,
+              HandleError(BalsaFrameEnums::INVALID_CHUNK_EXTENSION));
+  ASSERT_EQ(message_headers.size(),
+            balsa_frame_.ProcessInput(message.data(), message.size()));
+  balsa_frame_.ProcessInput(message.data() + message_headers.size(),
+                            message.size());
+}
+
 TEST_F(HTTPBalsaFrameTest,
        VisitorInvokedProperlyForRequestWithTransferEncoding) {
   std::string message_headers =
diff --git a/quiche/balsa/http_validation_policy.h b/quiche/balsa/http_validation_policy.h
index 9ea158e..b35799a 100644
--- a/quiche/balsa/http_validation_policy.h
+++ b/quiche/balsa/http_validation_policy.h
@@ -59,6 +59,10 @@
   // neither, depending on InvalidCharsLevel, if a request header value contains
   // a carriage return that is not succeeded by a line feed.
   bool disallow_lone_cr_in_request_headers = false;
+
+  // The RFC is quite specific about chunk extensions formatting, but we only
+  // verify that there are no CR without a subsequent LF.
+  bool disallow_lone_cr_in_chunk_extension = false;
 };
 
 }  // namespace quiche