Adds an option for maximum header field size to OgHttp2Adapter. This option will be used during migration for compatibility with nghttp2. PiperOrigin-RevId: 417527069
diff --git a/http2/adapter/header_validator.cc b/http2/adapter/header_validator.cc index 0b042b1..3a70628 100644 --- a/http2/adapter/header_validator.cc +++ b/http2/adapter/header_validator.cc
@@ -61,6 +61,12 @@ if (key.empty()) { return HEADER_FIELD_INVALID; } + if (max_field_size_.has_value() && + key.size() + value.size() > max_field_size_.value()) { + QUICHE_VLOG(2) << "Header field size is " << key.size() + value.size() + << ", exceeds max size of " << max_field_size_.value(); + return HEADER_FIELD_INVALID; + } const absl::string_view validated_key = key[0] == ':' ? key.substr(1) : key; if (validated_key.find_first_not_of(kHttp2HeaderNameAllowedChars) != absl::string_view::npos) {
diff --git a/http2/adapter/header_validator.h b/http2/adapter/header_validator.h index a1d27bb..fc8c226 100644 --- a/http2/adapter/header_validator.h +++ b/http2/adapter/header_validator.h
@@ -5,6 +5,7 @@ #include <vector> #include "absl/strings/string_view.h" +#include "absl/types/optional.h" #include "common/platform/api/quiche_export.h" namespace http2 { @@ -22,6 +23,8 @@ public: HeaderValidator() {} + void SetMaxFieldSize(uint32_t field_size) { max_field_size_ = field_size; } + // If called, this validator will allow the `:protocol` pseudo-header, as // described in RFC 8441. void AllowConnect() { allow_connect_ = true; } @@ -46,6 +49,7 @@ std::vector<std::string> pseudo_headers_; std::string status_; std::string method_; + absl::optional<size_t> max_field_size_; bool allow_connect_ = false; };
diff --git a/http2/adapter/header_validator_test.cc b/http2/adapter/header_validator_test.cc index 1f202d9..09c4d43 100644 --- a/http2/adapter/header_validator_test.cc +++ b/http2/adapter/header_validator_test.cc
@@ -19,6 +19,18 @@ EXPECT_EQ(HeaderValidator::HEADER_OK, status); } +TEST(HeaderValidatorTest, ExceedsMaxSize) { + HeaderValidator v; + v.SetMaxFieldSize(64u); + HeaderValidator::HeaderStatus status = + v.ValidateSingleHeader("name", "value"); + EXPECT_EQ(HeaderValidator::HEADER_OK, status); + status = v.ValidateSingleHeader( + "name2", + "Antidisestablishmentariansism is supercalifragilisticexpialodocious."); + EXPECT_EQ(HeaderValidator::HEADER_FIELD_INVALID, status); +} + TEST(HeaderValidatorTest, NameHasInvalidChar) { HeaderValidator v; for (const bool is_pseudo_header : {true, false}) {
diff --git a/http2/adapter/nghttp2_adapter_test.cc b/http2/adapter/nghttp2_adapter_test.cc index 4e0400a..15b3495 100644 --- a/http2/adapter/nghttp2_adapter_test.cc +++ b/http2/adapter/nghttp2_adapter_test.cc
@@ -2668,6 +2668,57 @@ EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS})); } +TEST(NgHttp2AdapterTest, ServerReceivesTooLargeHeader) { + DataSavingVisitor visitor; + auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor); + + // nghttp2 will accept a maximum of 64kB of huffman encoded data per header + // field. + const std::string too_large_value = std::string(80 * 1024, 'q'); + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}, + {"x-toobig", too_large_value}}, + /*fin=*/false) + .Serialize(); + testing::InSequence s; + + // Client preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + // Further header processing is skipped, as the header field is too large. + + const int64_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(frames.size(), result); + + EXPECT_TRUE(adapter->want_write()); + + // Since nghttp2 opted not to process the header, it generates a GOAWAY with + // error code COMPRESSION_ERROR. + EXPECT_CALL(visitor, OnBeforeFrameSent(GOAWAY, 0, 8, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(GOAWAY, 0, 8, 0x0, + static_cast<int>(Http2ErrorCode::COMPRESSION_ERROR))); + + int send_result = adapter->Send(); + // Some bytes should have been serialized. + EXPECT_EQ(0, send_result); + // GOAWAY. + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::GOAWAY})); +} + TEST(NgHttp2AdapterTest, ServerSubmitResponse) { DataSavingVisitor visitor; auto adapter = NgHttp2Adapter::CreateServerAdapter(visitor);
diff --git a/http2/adapter/oghttp2_adapter_test.cc b/http2/adapter/oghttp2_adapter_test.cc index 4970f52..9635bdf 100644 --- a/http2/adapter/oghttp2_adapter_test.cc +++ b/http2/adapter/oghttp2_adapter_test.cc
@@ -3318,6 +3318,66 @@ spdy::SpdyFrameType::GOAWAY})); } +TEST(OgHttp2AdapterServerTest, ServerReceivesTooLargeHeader) { + DataSavingVisitor visitor; + OgHttp2Adapter::Options options{.perspective = Perspective::kServer, + .max_header_field_size = 64 * 1024}; + auto adapter = OgHttp2Adapter::Create(visitor, options); + + // Due to configuration, the library will accept a maximum of 64kB of huffman + // encoded data per header field. + const std::string too_large_value = std::string(80 * 1024, 'q'); + const std::string frames = TestFrameSequence() + .ClientPreface() + .Headers(1, + {{":method", "POST"}, + {":scheme", "https"}, + {":authority", "example.com"}, + {":path", "/this/is/request/one"}, + {"x-toobig", too_large_value}}, + /*fin=*/false) + .Serialize(); + testing::InSequence s; + + // Client preface (empty SETTINGS) + EXPECT_CALL(visitor, OnFrameHeader(0, 0, SETTINGS, 0)); + EXPECT_CALL(visitor, OnSettingsStart()); + EXPECT_CALL(visitor, OnSettingsEnd()); + + EXPECT_CALL(visitor, OnFrameHeader(1, _, HEADERS, 0)); + EXPECT_CALL(visitor, OnBeginHeadersForStream(1)); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":method", "POST")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":scheme", "https")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":authority", "example.com")); + EXPECT_CALL(visitor, OnHeaderForStream(1, ":path", "/this/is/request/one")); + EXPECT_CALL(visitor, OnConnectionError(ConnectionError::kParseError)); + // Further header processing is skipped, as the header field is too large. + + const int64_t result = adapter->ProcessBytes(frames); + EXPECT_EQ(static_cast<int64_t>(frames.size()), result); + + EXPECT_TRUE(adapter->want_write()); + + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x0)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x0, 0)); + EXPECT_CALL(visitor, OnBeforeFrameSent(SETTINGS, 0, _, 0x1)); + EXPECT_CALL(visitor, OnFrameSent(SETTINGS, 0, _, 0x1, 0)); + // Since the library opted not to process the header, it generates a GOAWAY + // with error code COMPRESSION_ERROR. + EXPECT_CALL(visitor, OnBeforeFrameSent(GOAWAY, 0, _, 0x0)); + EXPECT_CALL(visitor, + OnFrameSent(GOAWAY, 0, _, 0x0, + static_cast<int>(Http2ErrorCode::COMPRESSION_ERROR))); + + int send_result = adapter->Send(); + // Some bytes should have been serialized. + EXPECT_EQ(0, send_result); + // SETTINGS, SETTINGS ack, and GOAWAY. + EXPECT_THAT(visitor.data(), EqualsFrames({spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::SETTINGS, + spdy::SpdyFrameType::GOAWAY})); +} + TEST(OgHttp2AdapterServerTest, ServerRejectsStreamData) { DataSavingVisitor visitor; OgHttp2Adapter::Options options{.perspective = Perspective::kServer};
diff --git a/http2/adapter/oghttp2_session.cc b/http2/adapter/oghttp2_session.cc index 0357f7b..ec746d4 100644 --- a/http2/adapter/oghttp2_session.cc +++ b/http2/adapter/oghttp2_session.cc
@@ -313,6 +313,9 @@ remaining_preface_ = {spdy::kHttp2ConnectionHeaderPrefix, spdy::kHttp2ConnectionHeaderPrefixSize}; } + if (options_.max_header_field_size.has_value()) { + headers_handler_.SetMaxFieldSize(options_.max_header_field_size.value()); + } } OgHttp2Session::~OgHttp2Session() {}
diff --git a/http2/adapter/oghttp2_session.h b/http2/adapter/oghttp2_session.h index d9f00df..72e36e9 100644 --- a/http2/adapter/oghttp2_session.h +++ b/http2/adapter/oghttp2_session.h
@@ -43,6 +43,8 @@ absl::optional<size_t> max_hpack_encoding_table_capacity = absl::nullopt; // The maximum number of decoded header bytes that a stream can receive. absl::optional<uint32_t> max_header_list_bytes = absl::nullopt; + // The maximum size of an individual header field, including name and value. + absl::optional<uint32_t> max_header_field_size = absl::nullopt; // Whether to automatically send PING acks when receiving a PING. bool auto_ping_ack = true; // Whether (as server) to send a RST_STREAM NO_ERROR when sending a fin on @@ -257,6 +259,9 @@ return validator_.status_header(); } void AllowConnect() { validator_.AllowConnect(); } + void SetMaxFieldSize(uint32_t field_size) { + validator_.SetMaxFieldSize(field_size); + } private: OgHttp2Session& session_;