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_;