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.