Adds HTTP/2 header validation logic, encapsulated in the HeaderValidator class.
PiperOrigin-RevId: 401516908
diff --git a/http2/adapter/header_validator.cc b/http2/adapter/header_validator.cc
new file mode 100644
index 0000000..832c629
--- /dev/null
+++ b/http2/adapter/header_validator.cc
@@ -0,0 +1,101 @@
+#include "http2/adapter/header_validator.h"
+
+#include "absl/strings/escaping.h"
+#include "common/platform/api/quiche_logging.h"
+
+namespace http2 {
+namespace adapter {
+
+namespace {
+
+const absl::string_view kHttp2HeaderNameAllowedChars =
+ "!#$%&\'*+-.0123456789"
+ "^_`abcdefghijklmnopqrstuvwxyz|~";
+
+const absl::string_view kHttp2HeaderValueAllowedChars =
+ "\t "
+ "!\"#$%&'()*+,-./"
+ "0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
+ "abcdefghijklmnopqrstuvwxyz{|}~";
+
+const absl::string_view kHttp2StatusValueAllowedChars = "0123456789";
+
+// TODO(birenroy): Support websocket requests, which contain an extra
+// `:protocol` pseudo-header.
+bool ValidateRequestHeaders(const std::vector<std::string>& pseudo_headers) {
+ static const std::vector<std::string> kRequiredHeaders = []() {
+ return std::vector<std::string>(
+ {":authority", ":method", ":path", ":scheme"});
+ }();
+ return pseudo_headers == kRequiredHeaders;
+}
+
+bool ValidateResponseHeaders(const std::vector<std::string>& pseudo_headers) {
+ static const std::vector<std::string> kRequiredHeaders = []() {
+ return std::vector<std::string>({":status"});
+ }();
+ return pseudo_headers == kRequiredHeaders;
+}
+
+bool ValidateResponseTrailers(const std::vector<std::string>& pseudo_headers) {
+ return pseudo_headers.empty();
+}
+
+} // namespace
+
+void HeaderValidator::StartHeaderBlock() {
+ pseudo_headers_.clear();
+ status_.clear();
+}
+
+HeaderValidator::HeaderStatus HeaderValidator::ValidateSingleHeader(
+ absl::string_view key, absl::string_view value) {
+ if (key.empty()) {
+ return HEADER_NAME_EMPTY;
+ }
+ const absl::string_view validated_key = key[0] == ':' ? key.substr(1) : key;
+ if (validated_key.find_first_not_of(kHttp2HeaderNameAllowedChars) !=
+ absl::string_view::npos) {
+ QUICHE_VLOG(2) << "invalid chars in header name: ["
+ << absl::CEscape(validated_key) << "]";
+ return HEADER_NAME_INVALID_CHAR;
+ }
+ if (value.find_first_not_of(kHttp2HeaderValueAllowedChars) !=
+ absl::string_view::npos) {
+ QUICHE_VLOG(2) << "invalid chars in header value: [" << absl::CEscape(value)
+ << "]";
+ return HEADER_VALUE_INVALID_CHAR;
+ }
+ if (key[0] == ':') {
+ if (key == ":status") {
+ if (value.size() != 3 ||
+ value.find_first_not_of(kHttp2StatusValueAllowedChars) !=
+ absl::string_view::npos) {
+ QUICHE_VLOG(2) << "malformed status value: [" << absl::CEscape(value)
+ << "]";
+ return HEADER_VALUE_INVALID_CHAR;
+ }
+ status_ = std::string(value);
+ }
+ pseudo_headers_.push_back(std::string(key));
+ }
+ return HEADER_OK;
+}
+
+// Returns true if all required pseudoheaders and no extra pseudoheaders are
+// present for the given header type.
+bool HeaderValidator::FinishHeaderBlock(HeaderType type) {
+ std::sort(pseudo_headers_.begin(), pseudo_headers_.end());
+ switch (type) {
+ case HeaderType::REQUEST:
+ return ValidateRequestHeaders(pseudo_headers_);
+ case HeaderType::RESPONSE_100:
+ case HeaderType::RESPONSE:
+ return ValidateResponseHeaders(pseudo_headers_);
+ case HeaderType::RESPONSE_TRAILER:
+ return ValidateResponseTrailers(pseudo_headers_);
+ }
+}
+
+} // namespace adapter
+} // namespace http2
diff --git a/http2/adapter/header_validator.h b/http2/adapter/header_validator.h
new file mode 100644
index 0000000..2d7585e
--- /dev/null
+++ b/http2/adapter/header_validator.h
@@ -0,0 +1,49 @@
+#ifndef QUICHE_HTTP2_ADAPTER_HEADER_VALIDATOR_H_
+#define QUICHE_HTTP2_ADAPTER_HEADER_VALIDATOR_H_
+
+#include <string>
+#include <vector>
+
+#include "absl/strings/string_view.h"
+#include "common/platform/api/quiche_export.h"
+
+namespace http2 {
+namespace adapter {
+
+enum class HeaderType : uint8_t {
+ REQUEST,
+ RESPONSE_100,
+ RESPONSE,
+ RESPONSE_TRAILER,
+};
+
+class QUICHE_EXPORT_PRIVATE HeaderValidator {
+ public:
+ HeaderValidator() {}
+
+ void StartHeaderBlock();
+
+ enum HeaderStatus {
+ HEADER_OK,
+ HEADER_NAME_EMPTY,
+ HEADER_NAME_INVALID_CHAR,
+ HEADER_VALUE_INVALID_CHAR,
+ };
+ HeaderStatus ValidateSingleHeader(absl::string_view key,
+ absl::string_view value);
+
+ // Returns true if all required pseudoheaders and no extra pseudoheaders are
+ // present for the given header type.
+ bool FinishHeaderBlock(HeaderType type);
+
+ absl::string_view status_header() const { return status_; }
+
+ private:
+ std::vector<std::string> pseudo_headers_;
+ std::string status_;
+};
+
+} // namespace adapter
+} // namespace http2
+
+#endif // QUICHE_HTTP2_ADAPTER_HEADER_VALIDATOR_H_
diff --git a/http2/adapter/header_validator_test.cc b/http2/adapter/header_validator_test.cc
new file mode 100644
index 0000000..a2a4cd5
--- /dev/null
+++ b/http2/adapter/header_validator_test.cc
@@ -0,0 +1,212 @@
+#include "http2/adapter/header_validator.h"
+
+#include "common/platform/api/quiche_test.h"
+
+namespace http2 {
+namespace adapter {
+namespace test {
+
+TEST(HeaderValidatorTest, HeaderNameEmpty) {
+ HeaderValidator v;
+ HeaderValidator::HeaderStatus status = v.ValidateSingleHeader("", "value");
+ EXPECT_EQ(HeaderValidator::HEADER_NAME_EMPTY, status);
+}
+
+TEST(HeaderValidatorTest, HeaderValueEmpty) {
+ HeaderValidator v;
+ HeaderValidator::HeaderStatus status = v.ValidateSingleHeader("name", "");
+ EXPECT_EQ(HeaderValidator::HEADER_OK, status);
+}
+
+TEST(HeaderValidatorTest, NameHasInvalidChar) {
+ HeaderValidator v;
+ for (const bool is_pseudo_header : {true, false}) {
+ // These characters should be allowed. (Not exhaustive.)
+ for (const char* c : {"!", "3", "a", "_", "|", "~"}) {
+ const std::string name = is_pseudo_header ? absl::StrCat(":met", c, "hod")
+ : absl::StrCat("na", c, "me");
+ HeaderValidator::HeaderStatus status =
+ v.ValidateSingleHeader(name, "value");
+ EXPECT_EQ(HeaderValidator::HEADER_OK, status);
+ }
+ // These should not. (Not exhaustive.)
+ for (const char* c : {"\\", "<", ";", "[", "=", " ", "\r", "\n", ",", "\"",
+ "\x1F", "\x91"}) {
+ const std::string name = is_pseudo_header ? absl::StrCat(":met", c, "hod")
+ : absl::StrCat("na", c, "me");
+ HeaderValidator::HeaderStatus status =
+ v.ValidateSingleHeader(name, "value");
+ EXPECT_EQ(HeaderValidator::HEADER_NAME_INVALID_CHAR, status);
+ }
+ // Uppercase characters in header names should not be allowed.
+ const std::string uc_name = is_pseudo_header ? ":Method" : "Name";
+ HeaderValidator::HeaderStatus status =
+ v.ValidateSingleHeader(uc_name, "value");
+ EXPECT_EQ(HeaderValidator::HEADER_NAME_INVALID_CHAR, status);
+ }
+}
+
+TEST(HeaderValidatorTest, ValueHasInvalidChar) {
+ HeaderValidator v;
+ // These characters should be allowed. (Not exhaustive.)
+ for (const char* c :
+ {"!", "3", "a", "_", "|", "~", "\\", "<", ";", "[", "=", "A", "\t"}) {
+ HeaderValidator::HeaderStatus status =
+ v.ValidateSingleHeader("name", absl::StrCat("val", c, "ue"));
+ EXPECT_EQ(HeaderValidator::HEADER_OK, status);
+ }
+ // These should not.
+ for (const char* c : {"\r", "\n"}) {
+ HeaderValidator::HeaderStatus status =
+ v.ValidateSingleHeader("name", absl::StrCat("val", c, "ue"));
+ EXPECT_EQ(HeaderValidator::HEADER_VALUE_INVALID_CHAR, status);
+ }
+}
+
+TEST(HeaderValidatorTest, StatusHasInvalidChar) {
+ HeaderValidator v;
+
+ for (HeaderType type : {HeaderType::RESPONSE, HeaderType::RESPONSE_100}) {
+ // When `:status` has a non-digit value, validation will fail.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_VALUE_INVALID_CHAR,
+ v.ValidateSingleHeader(":status", "bar"));
+ EXPECT_FALSE(v.FinishHeaderBlock(type));
+
+ // When `:status` is too short, validation will fail.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_VALUE_INVALID_CHAR,
+ v.ValidateSingleHeader(":status", "10"));
+ EXPECT_FALSE(v.FinishHeaderBlock(type));
+
+ // When `:status` is too long, validation will fail.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_VALUE_INVALID_CHAR,
+ v.ValidateSingleHeader(":status", "9000"));
+ EXPECT_FALSE(v.FinishHeaderBlock(type));
+
+ // When `:status` is just right, validation will succeed.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":status", "400"));
+ EXPECT_TRUE(v.FinishHeaderBlock(type));
+ }
+}
+
+TEST(HeaderValidatorTest, RequestPseudoHeaders) {
+ HeaderValidator v;
+ const absl::string_view headers[] = {":authority", ":method", ":path",
+ ":scheme"};
+ for (absl::string_view to_skip : headers) {
+ v.StartHeaderBlock();
+ for (absl::string_view to_add : headers) {
+ if (to_add != to_skip) {
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(to_add, "foo"));
+ }
+ }
+ // When any pseudo-header is missing, final validation will fail.
+ EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST));
+ }
+
+ // When all pseudo-headers are present, final validation will succeed.
+ v.StartHeaderBlock();
+ for (absl::string_view to_add : headers) {
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(to_add, "foo"));
+ }
+ EXPECT_TRUE(v.FinishHeaderBlock(HeaderType::REQUEST));
+
+ // When an extra pseudo-header is present, final validation will fail.
+ v.StartHeaderBlock();
+ for (absl::string_view to_add : headers) {
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(to_add, "foo"));
+ }
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":extra", "blah"));
+ EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST));
+
+ // When a required pseudo-header is repeated, final validation will fail.
+ for (absl::string_view to_repeat : headers) {
+ v.StartHeaderBlock();
+ for (absl::string_view to_add : headers) {
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(to_add, "foo"));
+ if (to_add == to_repeat) {
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(to_add, "foo"));
+ }
+ }
+ EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST));
+ }
+}
+
+TEST(HeaderValidatorTest, WebsocketPseudoHeaders) {
+ HeaderValidator v;
+ const absl::string_view headers[] = {":authority", ":method", ":path",
+ ":scheme"};
+ v.StartHeaderBlock();
+ for (absl::string_view to_add : headers) {
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(to_add, "foo"));
+ }
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":protocol", "websocket"));
+ // For now, `:protocol` is treated as an extra pseudo-header.
+ EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::REQUEST));
+}
+
+TEST(HeaderValidatorTest, ResponsePseudoHeaders) {
+ HeaderValidator v;
+
+ for (HeaderType type : {HeaderType::RESPONSE, HeaderType::RESPONSE_100}) {
+ // When `:status` is missing, validation will fail.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_OK, v.ValidateSingleHeader("foo", "bar"));
+ EXPECT_FALSE(v.FinishHeaderBlock(type));
+
+ // When all pseudo-headers are present, final validation will succeed.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":status", "199"));
+ EXPECT_TRUE(v.FinishHeaderBlock(type));
+ EXPECT_EQ("199", v.status_header());
+
+ // When `:status` is repeated, validation will fail.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":status", "199"));
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":status", "299"));
+ EXPECT_FALSE(v.FinishHeaderBlock(type));
+
+ // When an extra pseudo-header is present, final validation will fail.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":status", "199"));
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":extra", "blorp"));
+ EXPECT_FALSE(v.FinishHeaderBlock(type));
+ }
+}
+
+TEST(HeaderValidatorTest, ResponseTrailerPseudoHeaders) {
+ HeaderValidator v;
+
+ // When no pseudo-headers are present, validation will succeed.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_OK, v.ValidateSingleHeader("foo", "bar"));
+ EXPECT_TRUE(v.FinishHeaderBlock(HeaderType::RESPONSE_TRAILER));
+
+ // When any pseudo-header is present, final validation will fail.
+ v.StartHeaderBlock();
+ EXPECT_EQ(HeaderValidator::HEADER_OK,
+ v.ValidateSingleHeader(":status", "200"));
+ EXPECT_EQ(HeaderValidator::HEADER_OK, v.ValidateSingleHeader("foo", "bar"));
+ EXPECT_FALSE(v.FinishHeaderBlock(HeaderType::RESPONSE_TRAILER));
+}
+
+} // namespace test
+} // namespace adapter
+} // namespace http2
diff --git a/http2/adapter/oghttp2_session.cc b/http2/adapter/oghttp2_session.cc
index 450a56d..e3a696b 100644
--- a/http2/adapter/oghttp2_session.cc
+++ b/http2/adapter/oghttp2_session.cc
@@ -131,24 +131,39 @@
} // namespace
void OgHttp2Session::PassthroughHeadersHandler::OnHeaderBlockStart() {
+ result_ = Http2VisitorInterface::HEADER_OK;
const bool status = visitor_.OnBeginHeadersForStream(stream_id_);
if (!status) {
result_ = Http2VisitorInterface::HEADER_CONNECTION_ERROR;
}
+ validator_.StartHeaderBlock();
}
void OgHttp2Session::PassthroughHeadersHandler::OnHeader(
absl::string_view key,
absl::string_view value) {
- if (result_ == Http2VisitorInterface::HEADER_OK) {
- result_ = visitor_.OnHeaderForStream(stream_id_, key, value);
+ if (result_ != Http2VisitorInterface::HEADER_OK) {
+ QUICHE_VLOG(2) << "Early return; status not HEADER_OK";
+ return;
}
+ const auto validation_result = validator_.ValidateSingleHeader(key, value);
+ if (validation_result != HeaderValidator::HEADER_OK) {
+ QUICHE_VLOG(2) << "RST_STREAM: invalid header found";
+ result_ = Http2VisitorInterface::HEADER_RST_STREAM;
+ return;
+ }
+ result_ = visitor_.OnHeaderForStream(stream_id_, key, value);
}
void OgHttp2Session::PassthroughHeadersHandler::OnHeaderBlockEnd(
size_t /* uncompressed_header_bytes */,
size_t /* compressed_header_bytes */) {
if (result_ == Http2VisitorInterface::HEADER_OK) {
+ if (!validator_.FinishHeaderBlock(type_)) {
+ result_ = Http2VisitorInterface::HEADER_RST_STREAM;
+ }
+ }
+ if (result_ == Http2VisitorInterface::HEADER_OK) {
const bool result = visitor_.OnEndHeadersForStream(stream_id_);
if (!result) {
session_.decoder_.StopProcessing();
@@ -670,10 +685,27 @@
spdy::SpdyHeadersHandlerInterface* OgHttp2Session::OnHeaderFrameStart(
spdy::SpdyStreamId stream_id) {
headers_handler_.set_stream_id(stream_id);
+ auto it = stream_map_.find(stream_id);
+ if (it != stream_map_.end()) {
+ headers_handler_.set_header_type(
+ NextHeaderType(it->second.received_header_type));
+ }
return &headers_handler_;
}
-void OgHttp2Session::OnHeaderFrameEnd(spdy::SpdyStreamId /*stream_id*/) {
+void OgHttp2Session::OnHeaderFrameEnd(spdy::SpdyStreamId stream_id) {
+ auto it = stream_map_.find(stream_id);
+ if (it != stream_map_.end()) {
+ if (headers_handler_.header_type() == HeaderType::RESPONSE &&
+ !headers_handler_.status_header().empty() &&
+ headers_handler_.status_header()[0] == '1') {
+ // If response headers carried a 1xx response code, final response headers
+ // should still be forthcoming.
+ it->second.received_header_type = HeaderType::RESPONSE_100;
+ } else {
+ it->second.received_header_type = headers_handler_.header_type();
+ }
+ }
headers_handler_.set_stream_id(0);
}
@@ -959,6 +991,18 @@
return stream_map_.size() < max_outbound_concurrent_streams_;
}
+HeaderType OgHttp2Session::NextHeaderType(
+ absl::optional<HeaderType> current_type) {
+ if (IsServerSession()) {
+ return HeaderType::REQUEST;
+ } else if (!current_type ||
+ current_type.value() == HeaderType::RESPONSE_100) {
+ return HeaderType::RESPONSE;
+ } else {
+ return HeaderType::RESPONSE_TRAILER;
+ }
+}
+
void OgHttp2Session::LatchErrorAndNotify() {
latched_error_ = true;
visitor_.OnConnectionError();
diff --git a/http2/adapter/oghttp2_session.h b/http2/adapter/oghttp2_session.h
index 558b36c..75f9c96 100644
--- a/http2/adapter/oghttp2_session.h
+++ b/http2/adapter/oghttp2_session.h
@@ -5,6 +5,7 @@
#include <list>
#include "http2/adapter/data_source.h"
+#include "http2/adapter/header_validator.h"
#include "http2/adapter/http2_protocol.h"
#include "http2/adapter/http2_session.h"
#include "http2/adapter/http2_util.h"
@@ -181,6 +182,7 @@
std::unique_ptr<spdy::SpdyHeaderBlock> trailers;
void* user_data = nullptr;
int32_t send_window = kInitialFlowControlWindowSize;
+ absl::optional<HeaderType> received_header_type;
bool half_closed_local = false;
bool half_closed_remote = false;
};
@@ -199,14 +201,24 @@
explicit PassthroughHeadersHandler(OgHttp2Session& session,
Http2VisitorInterface& visitor)
: session_(session), visitor_(visitor) {}
+
void set_stream_id(Http2StreamId stream_id) {
stream_id_ = stream_id;
result_ = Http2VisitorInterface::HEADER_OK;
}
+
+ void set_header_type(HeaderType type) { type_ = type; }
+ HeaderType header_type() const { return type_; }
+
void OnHeaderBlockStart() override;
void OnHeader(absl::string_view key, absl::string_view value) override;
void OnHeaderBlockEnd(size_t /* uncompressed_header_bytes */,
size_t /* compressed_header_bytes */) override;
+ absl::string_view status_header() {
+ QUICHE_DCHECK(type_ == HeaderType::RESPONSE ||
+ type_ == HeaderType::RESPONSE_100);
+ return validator_.status_header();
+ }
private:
OgHttp2Session& session_;
@@ -214,6 +226,9 @@
Http2StreamId stream_id_ = 0;
Http2VisitorInterface::OnHeaderResult result_ =
Http2VisitorInterface::HEADER_OK;
+ // Validates header blocks according to the HTTP/2 specification.
+ HeaderValidator validator_;
+ HeaderType type_ = HeaderType::RESPONSE;
};
// Queues the connection preface, if not already done.
@@ -256,6 +271,9 @@
// Closes the given `stream_id` with the given `error_code`.
void CloseStream(Http2StreamId stream_id, Http2ErrorCode error_code);
+ // Calculates the next expected header type for a stream in a given state.
+ HeaderType NextHeaderType(absl::optional<HeaderType> current_type);
+
// Returns true if the session can create a new stream.
bool CanCreateStream() const;