blob: 211d26452bc203ab64effcbd45909a5ce8a0d65b [file] [log] [blame]
#include "quiche/http2/adapter/header_validator.h"
#include <array>
#include <bitset>
#include "absl/strings/ascii.h"
#include "absl/strings/escaping.h"
#include "absl/strings/numbers.h"
#include "absl/strings/str_cat.h"
#include "quiche/http2/adapter/header_validator_base.h"
#include "quiche/http2/http2_constants.h"
#include "quiche/common/platform/api/quiche_logging.h"
namespace http2 {
namespace adapter {
namespace {
// From RFC 9110 Section 5.6.2.
constexpr absl::string_view kHttpTokenChars =
"!#$%&'*+-.^_`|~0123456789"
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
constexpr absl::string_view kHttp2HeaderNameAllowedChars =
"!#$%&'*+-.0123456789"
"^_`abcdefghijklmnopqrstuvwxyz|~";
constexpr absl::string_view kHttp2HeaderValueAllowedChars =
"\t "
"!\"#$%&'()*+,-./"
"0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`"
"abcdefghijklmnopqrstuvwxyz{|}~";
constexpr absl::string_view kHttp2StatusValueAllowedChars = "0123456789";
constexpr absl::string_view kValidAuthorityChars =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()["
"]*+,;=:";
constexpr absl::string_view kValidPathChars =
"/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()"
"*+,;=:@?";
constexpr absl::string_view kValidPathCharsWithFragment =
"/abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~%!$&'()"
"*+,;=:@?#";
using CharMap = std::array<bool, 256>;
constexpr CharMap BuildValidCharMap(absl::string_view valid_chars) {
CharMap map = {};
for (char c : valid_chars) {
// An array index must be a nonnegative integer, hence the cast to uint8_t.
map[static_cast<uint8_t>(c)] = true;
}
return map;
}
constexpr CharMap AllowObsText(CharMap map) {
// Characters above 0x80 are allowed in header field values as `obs-text` in
// RFC 7230.
for (uint8_t c = 0xff; c >= 0x80; --c) {
map[c] = true;
}
return map;
}
bool AllCharsInMap(absl::string_view str, const CharMap& map) {
for (char c : str) {
if (!map[static_cast<uint8_t>(c)]) {
return false;
}
}
return true;
}
bool IsValidStatus(absl::string_view status) {
static constexpr CharMap valid_chars =
BuildValidCharMap(kHttp2StatusValueAllowedChars);
return AllCharsInMap(status, valid_chars);
}
bool IsValidMethod(absl::string_view method) {
static constexpr CharMap valid_chars = BuildValidCharMap(kHttpTokenChars);
return AllCharsInMap(method, valid_chars);
}
} // namespace
void HeaderValidator::StartHeaderBlock() {
HeaderValidatorBase::StartHeaderBlock();
pseudo_headers_.reset();
pseudo_header_state_.reset();
authority_.clear();
}
void HeaderValidator::RecordPseudoHeader(PseudoHeaderTag tag) {
if (pseudo_headers_[tag]) {
pseudo_headers_[TAG_UNKNOWN_EXTRA] = true;
} else {
pseudo_headers_[tag] = true;
}
}
HeaderValidator::HeaderStatus HeaderValidator::ValidateSingleHeader(
absl::string_view key, absl::string_view value) {
if (key.empty()) {
return HEADER_FIELD_INVALID;
}
if (max_field_size_.has_value() &&
key.size() + value.size() > *max_field_size_) {
QUICHE_VLOG(2) << "Header field size is " << key.size() + value.size()
<< ", exceeds max size of " << *max_field_size_;
return HEADER_FIELD_TOO_LONG;
}
if (key[0] == ':') {
// Remove leading ':'.
key.remove_prefix(1);
if (key == "status") {
if (value.size() != 3 || !IsValidStatus(value)) {
QUICHE_VLOG(2) << "malformed status value: [" << absl::CEscape(value)
<< "]";
return HEADER_FIELD_INVALID;
}
if (value == "101") {
// Switching protocols is not allowed on a HTTP/2 stream.
return HEADER_FIELD_INVALID;
}
status_ = std::string(value);
RecordPseudoHeader(TAG_STATUS);
} else if (key == "method") {
if (value == "OPTIONS") {
pseudo_header_state_[STATE_METHOD_IS_OPTIONS] = true;
} else if (value == "CONNECT") {
pseudo_header_state_[STATE_METHOD_IS_CONNECT] = true;
} else if (!IsValidMethod(value)) {
return HEADER_FIELD_INVALID;
}
RecordPseudoHeader(TAG_METHOD);
} else if (key == "authority") {
if (!ValidateAndSetAuthority(value)) {
return HEADER_FIELD_INVALID;
}
RecordPseudoHeader(TAG_AUTHORITY);
} else if (key == "path") {
if (value == "*") {
pseudo_header_state_[STATE_PATH_IS_STAR] = true;
} else if (value.empty()) {
pseudo_header_state_[STATE_PATH_IS_EMPTY] = true;
return HEADER_FIELD_INVALID;
} else if (validate_path_ &&
!IsValidPath(value, allow_fragment_in_path_)) {
return HEADER_FIELD_INVALID;
}
if (value[0] == '/') {
pseudo_header_state_[STATE_PATH_INITIAL_SLASH] = true;
}
RecordPseudoHeader(TAG_PATH);
} else if (key == "protocol") {
RecordPseudoHeader(TAG_PROTOCOL);
} else if (key == "scheme") {
RecordPseudoHeader(TAG_SCHEME);
} else {
pseudo_headers_[TAG_UNKNOWN_EXTRA] = true;
if (!IsValidHeaderName(key)) {
QUICHE_VLOG(2) << "invalid chars in header name: ["
<< absl::CEscape(key) << "]";
return HEADER_FIELD_INVALID;
}
}
if (!IsValidHeaderValue(value, obs_text_option_)) {
QUICHE_VLOG(2) << "invalid chars in header value: ["
<< absl::CEscape(value) << "]";
return HEADER_FIELD_INVALID;
}
} else {
std::string lowercase_key;
if (allow_uppercase_in_header_names_) {
// Convert header name to lowercase for validation and also for comparison
// to lowercase string literals below.
lowercase_key = absl::AsciiStrToLower(key);
key = lowercase_key;
}
if (!IsValidHeaderName(key)) {
QUICHE_VLOG(2) << "invalid chars in header name: [" << absl::CEscape(key)
<< "]";
return HEADER_FIELD_INVALID;
}
if (!IsValidHeaderValue(value, obs_text_option_)) {
QUICHE_VLOG(2) << "invalid chars in header value: ["
<< absl::CEscape(value) << "]";
return HEADER_FIELD_INVALID;
}
if (key == "host") {
if (pseudo_headers_[TAG_STATUS]) {
// Response headers can contain "Host".
} else {
if (!ValidateAndSetAuthority(value)) {
return HEADER_FIELD_INVALID;
}
pseudo_headers_[TAG_AUTHORITY] = true;
}
} else if (key == "content-length") {
const ContentLengthStatus status = HandleContentLength(value);
switch (status) {
case CONTENT_LENGTH_ERROR:
return HEADER_FIELD_INVALID;
case CONTENT_LENGTH_SKIP:
return HEADER_SKIP;
case CONTENT_LENGTH_OK:
return HEADER_OK;
default:
return HEADER_FIELD_INVALID;
}
} else if (key == "te" && value != "trailers") {
return HEADER_FIELD_INVALID;
} else if (key == "upgrade" || GetInvalidHttp2HeaderSet().contains(key)) {
// TODO(b/78024822): Remove the "upgrade" here once it's added to
// GetInvalidHttp2HeaderSet().
return HEADER_FIELD_INVALID;
}
}
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) {
switch (type) {
case HeaderType::REQUEST:
return ValidateRequestHeaders(pseudo_headers_, pseudo_header_state_,
allow_extended_connect_);
case HeaderType::REQUEST_TRAILER:
return ValidateRequestTrailers(pseudo_headers_);
case HeaderType::RESPONSE_100:
case HeaderType::RESPONSE:
return ValidateResponseHeaders(pseudo_headers_);
case HeaderType::RESPONSE_TRAILER:
return ValidateResponseTrailers(pseudo_headers_);
}
return false;
}
bool HeaderValidator::IsValidHeaderName(absl::string_view name) {
static constexpr CharMap valid_chars =
BuildValidCharMap(kHttp2HeaderNameAllowedChars);
return AllCharsInMap(name, valid_chars);
}
bool HeaderValidator::IsValidHeaderValue(absl::string_view value,
ObsTextOption option) {
static constexpr CharMap valid_chars =
BuildValidCharMap(kHttp2HeaderValueAllowedChars);
static constexpr CharMap valid_chars_with_obs_text =
AllowObsText(BuildValidCharMap(kHttp2HeaderValueAllowedChars));
return AllCharsInMap(value, option == ObsTextOption::kAllow
? valid_chars_with_obs_text
: valid_chars);
}
bool HeaderValidator::IsValidAuthority(absl::string_view authority) {
static constexpr CharMap valid_chars =
BuildValidCharMap(kValidAuthorityChars);
return AllCharsInMap(authority, valid_chars);
}
bool HeaderValidator::IsValidPath(absl::string_view path, bool allow_fragment) {
static constexpr CharMap valid_chars = BuildValidCharMap(kValidPathChars);
static constexpr CharMap valid_chars_with_fragment =
BuildValidCharMap(kValidPathCharsWithFragment);
if (allow_fragment) {
return AllCharsInMap(path, valid_chars_with_fragment);
} else {
return AllCharsInMap(path, valid_chars);
}
}
HeaderValidator::ContentLengthStatus HeaderValidator::HandleContentLength(
absl::string_view value) {
if (value.empty()) {
return CONTENT_LENGTH_ERROR;
}
if (status_ == "204" && value != "0") {
// There should be no body in a "204 No Content" response.
return CONTENT_LENGTH_ERROR;
}
if (!status_.empty() && status_[0] == '1' && value != "0") {
// There should also be no body in a 1xx response.
return CONTENT_LENGTH_ERROR;
}
size_t content_length = 0;
const bool valid = absl::SimpleAtoi(value, &content_length);
if (!valid) {
return CONTENT_LENGTH_ERROR;
}
if (content_length_.has_value()) {
return content_length == *content_length_ ? CONTENT_LENGTH_SKIP
: CONTENT_LENGTH_ERROR;
}
content_length_ = content_length;
return CONTENT_LENGTH_OK;
}
// Returns whether `authority` contains only characters from the `host` ABNF
// from RFC 3986 section 3.2.2.
bool HeaderValidator::ValidateAndSetAuthority(absl::string_view authority) {
if (!IsValidAuthority(authority)) {
return false;
}
if (!allow_different_host_and_authority_ && pseudo_headers_[TAG_AUTHORITY] &&
authority != authority_) {
return false;
}
if (!authority.empty()) {
pseudo_header_state_[STATE_AUTHORITY_IS_NONEMPTY] = true;
if (authority_.empty()) {
authority_ = authority;
} else {
absl::StrAppend(&authority_, ", ", authority);
}
}
return true;
}
bool HeaderValidator::ValidateRequestHeaders(
const PseudoHeaderTagSet& pseudo_headers,
const PseudoHeaderStateSet& pseudo_header_state,
bool allow_extended_connect) {
QUICHE_VLOG(2) << "Request pseudo-headers: [" << pseudo_headers
<< "], pseudo_header_state: [" << pseudo_header_state
<< "], allow_extended_connect: " << allow_extended_connect;
if (pseudo_header_state[STATE_METHOD_IS_CONNECT]) {
if (allow_extended_connect) {
// See RFC 8441. Extended CONNECT should have: authority, method, path,
// protocol and scheme pseudo-headers. The tags corresponding to status
// and unknown_extra should not be set.
static const auto* kExtendedConnectHeaders =
new PseudoHeaderTagSet(0b0011111);
if (pseudo_headers == *kExtendedConnectHeaders) {
return true;
}
}
// See RFC 7540 Section 8.3. Regular CONNECT should have authority and
// method, but no other pseudo headers.
static const auto* kConnectHeaders = new PseudoHeaderTagSet(0b0000011);
return pseudo_header_state[STATE_AUTHORITY_IS_NONEMPTY] &&
pseudo_headers == *kConnectHeaders;
}
if (pseudo_header_state[STATE_PATH_IS_EMPTY]) {
return false;
}
if (pseudo_header_state[STATE_PATH_IS_STAR]) {
if (!pseudo_header_state[STATE_METHOD_IS_OPTIONS]) {
return false;
}
} else if (!pseudo_header_state[STATE_PATH_INITIAL_SLASH]) {
return false;
}
// Regular HTTP requests require authority, method, path and scheme.
static const auto* kRequiredHeaders = new PseudoHeaderTagSet(0b0010111);
return pseudo_headers == *kRequiredHeaders;
}
bool HeaderValidator::ValidateRequestTrailers(
const PseudoHeaderTagSet& pseudo_headers) {
return pseudo_headers.none();
}
bool HeaderValidator::ValidateResponseHeaders(
const PseudoHeaderTagSet& pseudo_headers) {
// HTTP responses require only the status pseudo header.
static const auto* kRequiredHeaders = new PseudoHeaderTagSet(0b0100000);
return pseudo_headers == *kRequiredHeaders;
}
bool HeaderValidator::ValidateResponseTrailers(
const PseudoHeaderTagSet& pseudo_headers) {
return pseudo_headers.none();
}
} // namespace adapter
} // namespace http2