Disallow colon in received header names (other than leading colon of pseudo-headers).

Protected by FLAGS_quic_reloadable_flag_quic_colon_invalid_in_header_name.

PiperOrigin-RevId: 576163167
diff --git a/quiche/quic/core/http/quic_spdy_stream.cc b/quiche/quic/core/http/quic_spdy_stream.cc
index 5f0701f..960b7e0 100644
--- a/quiche/quic/core/http/quic_spdy_stream.cc
+++ b/quiche/quic/core/http/quic_spdy_stream.cc
@@ -35,6 +35,7 @@
 #include "quiche/quic/platform/api/quic_logging.h"
 #include "quiche/quic/platform/api/quic_testvalue.h"
 #include "quiche/common/capsule.h"
+#include "quiche/common/platform/api/quiche_flag_utils.h"
 #include "quiche/common/platform/api/quiche_logging.h"
 #include "quiche/common/quiche_mem_slice_storage.h"
 #include "quiche/common/quiche_text_utils.h"
@@ -1640,11 +1641,12 @@
 }
 
 namespace {
+
 // Return true if |c| is not allowed in an HTTP/3 wire-encoded header and
 // pseudo-header names according to
 // https://datatracker.ietf.org/doc/html/draft-ietf-quic-http#section-4.1.1 and
 // https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-semantics-19#section-5.6.2
-constexpr bool isInvalidHeaderNameCharacter(unsigned char c) {
+constexpr bool IsInvalidHeaderNameCharacter(unsigned char c) {
   if (c == '!' || c == '|' || c == '~' || c == '*' || c == '+' || c == '-' ||
       c == '.' ||
       // #, $, %, &, '
@@ -1657,6 +1659,37 @@
   }
   return true;
 }
+
+// Return true if `name` is invalid because it contains a disallowed character.
+bool HeaderNameHasInvalidCharacter(absl::string_view name) {
+  const bool colon_invalid =
+      GetQuicReloadableFlag(quic_colon_invalid_in_header_name);
+  if (colon_invalid) {
+    QUICHE_RELOADABLE_FLAG_COUNT(quic_colon_invalid_in_header_name);
+  }
+
+  if (name.empty()) {
+    return false;
+  }
+
+  // Remove leading colon of pseudo-headers.
+  // This is the only position where colon is allowed.
+  if (name[0] == ':') {
+    name.remove_prefix(1);
+  }
+
+  if (std::find(name.begin(), name.end(), ':') != name.end()) {
+    // Header name contains colon (other than optional leading colon of
+    // pseudo-headers), which is invalid.
+    QUICHE_CODE_COUNT(quic_colon_in_header_name);
+    if (colon_invalid) {
+      return true;
+    }
+  }
+
+  return std::any_of(name.begin(), name.end(), IsInvalidHeaderNameCharacter);
+}
+
 }  // namespace
 
 bool QuicSpdyStream::ValidateReceivedHeaders(
@@ -1673,8 +1706,9 @@
   bool is_response = false;
   for (const std::pair<std::string, std::string>& pair : header_list) {
     const std::string& name = pair.first;
-    if (std::any_of(name.begin(), name.end(), isInvalidHeaderNameCharacter)) {
-      invalid_request_details_ = absl::StrCat("Invalid request header ", name);
+    if (HeaderNameHasInvalidCharacter(name)) {
+      invalid_request_details_ =
+          absl::StrCat("Invalid character in header name ", name);
       QUIC_DLOG(ERROR) << invalid_request_details_;
       return false;
     }
diff --git a/quiche/quic/core/http/quic_spdy_stream_test.cc b/quiche/quic/core/http/quic_spdy_stream_test.cc
index 3e0b8a1..3073703 100644
--- a/quiche/quic/core/http/quic_spdy_stream_test.cc
+++ b/quiche/quic/core/http/quic_spdy_stream_test.cc
@@ -231,7 +231,9 @@
   ~TestStream() override = default;
 
   using QuicSpdyStream::set_ack_listener;
+  using QuicSpdyStream::ValidateReceivedHeaders;
   using QuicStream::CloseWriteSide;
+  using QuicStream::sequencer;
   using QuicStream::WriteOrBufferData;
 
   void OnBodyAvailable() override {
@@ -266,11 +268,6 @@
   const std::string& data() const { return data_; }
   const spdy::Http2HeaderBlock& saved_headers() const { return saved_headers_; }
 
-  // Expose protected accessor.
-  const QuicStreamSequencer* sequencer() const {
-    return QuicStream::sequencer();
-  }
-
   void OnStreamHeaderList(bool fin, size_t frame_len,
                           const QuicHeaderList& header_list) override {
     headers_payload_length_ = frame_len;
@@ -3424,6 +3421,32 @@
   EXPECT_EQ(0u, bytes_read);
 }
 
+TEST_P(QuicSpdyStreamTest, ColonAllowedInHeaderName) {
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  SetQuicReloadableFlag(quic_colon_invalid_in_header_name, false);
+  Initialize(kShouldProcessData);
+
+  headers_["foo:bar"] = "invalid";
+  EXPECT_TRUE(stream_->ValidateReceivedHeaders(AsHeaderList(headers_)));
+}
+
+TEST_P(QuicSpdyStreamTest, ColonDisallowedInHeaderName) {
+  if (!UsesHttp3()) {
+    return;
+  }
+
+  SetQuicReloadableFlag(quic_colon_invalid_in_header_name, true);
+  Initialize(kShouldProcessData);
+
+  headers_["foo:bar"] = "invalid";
+  EXPECT_FALSE(stream_->ValidateReceivedHeaders(AsHeaderList(headers_)));
+  EXPECT_EQ("Invalid character in header name foo:bar",
+            stream_->invalid_request_details());
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quiche/quic/core/quic_flags_list.h b/quiche/quic/core/quic_flags_list.h
index 74588ea..0281a1f 100644
--- a/quiche/quic/core/quic_flags_list.h
+++ b/quiche/quic/core/quic_flags_list.h
@@ -69,6 +69,8 @@
 QUIC_FLAG(quic_reloadable_flag_quic_disable_server_blackhole_detection, false)
 // If true, disable resumption when receiving NRES connection option.
 QUIC_FLAG(quic_reloadable_flag_quic_enable_disable_resumption, true)
+// If true, disallow colon in received header names (other than leading colon of pseudo-headers).
+QUIC_FLAG(quic_reloadable_flag_quic_colon_invalid_in_header_name, true)
 // If true, discard INITIAL packet if the key has been dropped.
 QUIC_FLAG(quic_reloadable_flag_quic_discard_initial_packet_with_key_dropped, true)
 // If true, dispatcher sends error code QUIC_HANDSHAKE_FAILED_PACKETS_BUFFERED_TOO_LONG when handshake fails due to packets buffered for too long.