Option for rejecting isolated carriage returns in header values.

PiperOrigin-RevId: 619209119
diff --git a/quiche/balsa/balsa_frame.cc b/quiche/balsa/balsa_frame.cc
index a1d04b6..03bc994 100644
--- a/quiche/balsa/balsa_frame.cc
+++ b/quiche/balsa/balsa_frame.cc
@@ -545,6 +545,12 @@
       found_invalid = true;
       invalid_chars_[*c]++;
     }
+    if (*c == '\r' &&
+        http_validation_policy().disallow_lone_cr_in_request_headers &&
+        c + 1 < stream_end && *(c + 1) != '\n') {
+      found_invalid = true;
+      invalid_chars_[*c]++;
+    }
   }
 
   return found_invalid;
diff --git a/quiche/balsa/balsa_frame_test.cc b/quiche/balsa/balsa_frame_test.cc
index 0e81900..294e11f 100644
--- a/quiche/balsa/balsa_frame_test.cc
+++ b/quiche/balsa/balsa_frame_test.cc
@@ -803,7 +803,7 @@
 
 TEST(HTTPBalsaFrame, RequestLinesParsedProperly) {
   SCOPED_TRACE("Testing that lines are properly parsed.");
-  const char firstline[] = "GET / HTTP/1.1\r\n";
+  const char firstline[] = "GET / \rHTTP/1.1\r\n";
   const std::pair<std::string, std::string> headers[] = {
       std::pair<std::string, std::string>("foo", "bar"),
       std::pair<std::string, std::string>("duck", "water"),
@@ -840,6 +840,41 @@
   HeaderLineTestHelper(firstline, true, headers, headers_len, ":\t \t", "\r\n");
 }
 
+TEST(HTTPBalsaFrame, CarriageReturnIllegalInHeaders) {
+  HttpValidationPolicy policy{.disallow_lone_cr_in_request_headers = true};
+  BalsaHeaders balsa_headers;
+  BalsaFrame framer;
+  framer.set_is_request(true);
+  framer.set_balsa_headers(&balsa_headers);
+  framer.set_http_validation_policy(policy);
+  framer.set_invalid_chars_level(BalsaFrame::InvalidCharsLevel::kError);
+  const std::pair<std::string, std::string> headers[] = {
+      std::pair<std::string, std::string>("foo", "bar"),
+      std::pair<std::string, std::string>("trucks", "value-has-solo-\r-in it"),
+  };
+  std::string message =
+      CreateMessage("GET / \rHTTP/1.1\r\n", headers, 2, ":", "\r\n", "");
+  framer.ProcessInput(message.data(), message.size());
+  EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::INVALID_HEADER_CHARACTER);
+  // One carriage return in firstline, 1 in header value.
+  EXPECT_EQ(framer.get_invalid_chars().at('\r'), 2);
+}
+
+TEST(HTTPBalsaFrame, CarriageReturnIllegalInHeaderKey) {
+  BalsaHeaders balsa_headers;
+  BalsaFrame framer;
+  framer.set_is_request(true);
+  framer.set_balsa_headers(&balsa_headers);
+  framer.set_invalid_chars_level(BalsaFrame::InvalidCharsLevel::kError);
+  const std::pair<std::string, std::string> headers[] = {
+      std::pair<std::string, std::string>("tru\rcks", "along"),
+  };
+  std::string message =
+      CreateMessage("GET / HTTP/1.1\r\n", headers, 1, ":", "\r\n", "");
+  framer.ProcessInput(message.data(), message.size());
+  EXPECT_EQ(framer.ErrorCode(), BalsaFrameEnums::INVALID_HEADER_NAME_CHARACTER);
+}
+
 TEST(HTTPBalsaFrame, ResponseLinesParsedProperly) {
   SCOPED_TRACE("ResponseLineParsedProperly");
   const char firstline[] = "HTTP/1.0 200 A reason\tphrase\r\n";
diff --git a/quiche/balsa/http_validation_policy.h b/quiche/balsa/http_validation_policy.h
index 5b912a1..9ea158ec 100644
--- a/quiche/balsa/http_validation_policy.h
+++ b/quiche/balsa/http_validation_policy.h
@@ -54,6 +54,11 @@
   // invalid character. Invalid characters are always disallowed according to
   // InvalidCharsLevel in request headers.
   bool disallow_invalid_header_characters_in_response = false;
+
+  // If true, then signal an INVALID_HEADER_CHARACTER warning or error, or
+  // 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;
 };
 
 }  // namespace quiche