Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions changelogs/current.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ behavior_changes:
change: |
Changes the default value of ``envoy.reloadable_features.http2_use_oghttp2`` to true. This changes the codec used for HTTP/2
requests and responses. This behavior can be reverted by setting the feature to false.
- area: http2
change: |
Discard the ``Host`` header if the ``:authority`` header was received to bring Envoy into compliance with
https://www.rfc-editor.org/rfc/rfc9113#section-8.3.1 This behavioral change can be reverted by setting runtime flag
``envoy.reloadable_features.http2_discard_host_header`` to false.

minor_behavior_changes:
# *Changes that may cause incompatibilities for some users, but should not for most*
Expand Down
12 changes: 12 additions & 0 deletions source/common/http/http2/codec_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2129,6 +2129,18 @@ int ServerConnectionImpl::onHeader(const nghttp2_frame* frame, HeaderString&& na
// For a server connection, we should never get push promise frames.
ASSERT(frame->hd.type == NGHTTP2_HEADERS);
ASSERT(frame->headers.cat == NGHTTP2_HCAT_REQUEST || frame->headers.cat == NGHTTP2_HCAT_HEADERS);
if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.http2_discard_host_header")) {
StreamImpl* stream = getStreamUnchecked(frame->hd.stream_id);
if (stream && name == static_cast<absl::string_view>(Http::Headers::get().HostLegacy)) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit, maybe move this check above the line 2131

StreamImpl* stream = getStreamUnchecked(frame->hd.stream_id);
if (!stream) {
// We have seen 1 or 2 crashes where we get a headers callback but there is no associated
// stream data. I honestly am not sure how this can happen. However, from reading the nghttp2
// code it looks possible that inflate_header_block() can safely inflate headers for an already
// closed stream, but will still call the headers callback. Since that seems possible, we should
// ignore this case here.
// TODO(mattklein123): Figure out a test case that can hit this.
stats_.headers_cb_no_stream_.inc();
return 0;
}

although not sure that check still available for now.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveHeader is called from two places. I would need to duplicate this check. Maybe it is is better to keep it in one place only.

// Check if there is already the :authority header
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The http2 spec says that :authority must come before regular header fields and that reversing the order should be considered a malformed request. As long as the h2 parser actually rejects the reverse order, this looks good.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct. I have added a test to validate this.

const auto result = stream->headers().get(Http::Headers::get().Host);
if (!result.empty()) {
// Discard the host header value
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My understanding of https://www.rfc-editor.org/rfc/rfc9113#section-8.3.1 is that discarding host is not necessary, but it must have the same value as :authority. Should the discarding be done only if the host differs from :authority?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here are the statements related to the server:

  1. The recipient of an HTTP/2 request MUST NOT use the Host header field to determine the target URI if ":authority" is present.
  2. A server SHOULD treat a request as malformed if it contains a Host header field that identifies an entity that differs from the entity in the ":authority" pseudo-header field. (Envoy does not need to reject request with Host different from :authority)
  3. An intermediary that forwards a request over HTTP/2 MAY retain any Host header field. (Envoy does not need to retain the Host if it was present).

Note that Envoy can not have both :authority and Host headers in the header map, they are treated as one and the same. As a result discarding the Host header is compliant with RFC.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You got me convinced. I agree it makes sense to remove the header and protect it with a runtime flag.

return 0;
}
// Otherwise use host value as :authority
}
}
return saveHeader(frame, std::move(name), std::move(value));
}

Expand Down
1 change: 1 addition & 0 deletions source/common/runtime/runtime_features.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ RUNTIME_GUARD(envoy_reloadable_features_http1_allow_codec_error_response_after_1
RUNTIME_GUARD(envoy_reloadable_features_http1_connection_close_header_in_redirect);
RUNTIME_GUARD(envoy_reloadable_features_http1_use_balsa_parser);
RUNTIME_GUARD(envoy_reloadable_features_http2_decode_metadata_with_quiche);
RUNTIME_GUARD(envoy_reloadable_features_http2_discard_host_header);
RUNTIME_GUARD(envoy_reloadable_features_http2_use_oghttp2);
RUNTIME_GUARD(envoy_reloadable_features_http2_validate_authority_with_quiche);
RUNTIME_GUARD(envoy_reloadable_features_http_allow_partial_urls_in_referer);
Expand Down
6 changes: 3 additions & 3 deletions test/common/http/http2/http2_frame.h
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,9 @@ class Http2Frame {
ASSERT(size() >= HeaderSize);
setPayloadSize(size() - HeaderSize);
}
// Headers are directly encoded
void appendStaticHeader(StaticHeaderIndex index);
void appendHeaderWithoutIndexing(StaticHeaderIndex index, absl::string_view value);

private:
void buildHeader(Type type, uint32_t payload_size = 0, uint8_t flags = 0, uint32_t stream_id = 0);
Expand All @@ -277,9 +280,6 @@ class Http2Frame {
std::copy(data.begin(), data.end(), data_.begin() + 9);
}

// Headers are directly encoded
void appendStaticHeader(StaticHeaderIndex index);
void appendHeaderWithoutIndexing(StaticHeaderIndex index, absl::string_view value);
void appendEmptyHeader();

DataContainer data_;
Expand Down
51 changes: 49 additions & 2 deletions test/integration/multiplexed_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2199,7 +2199,7 @@ TEST_P(Http2FrameIntegrationTest, HostDifferentFromAuthority) {
sendFrame(request);

waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getHostValue(), "one.example.com,two.example.com");
EXPECT_EQ(upstream_request_->headers().getHostValue(), "one.example.com");
upstream_request_->encodeHeaders(default_response_headers_, true);
auto frame = readFrame();
EXPECT_EQ(Http2Frame::Type::Headers, frame.type());
Expand All @@ -2216,14 +2216,61 @@ TEST_P(Http2FrameIntegrationTest, HostSameAsAuthority) {
sendFrame(request);

waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getHostValue(), "one.example.com,one.example.com");
EXPECT_EQ(upstream_request_->headers().getHostValue(), "one.example.com");
upstream_request_->encodeHeaders(default_response_headers_, true);
auto frame = readFrame();
EXPECT_EQ(Http2Frame::Type::Headers, frame.type());
EXPECT_EQ(Http2Frame::ResponseStatus::Ok, frame.responseStatus());
tcp_client_->close();
}

TEST_P(Http2FrameIntegrationTest, HostConcatenatedWithAuthorityWithOverride) {
config_helper_.addRuntimeOverride("envoy.reloadable_features.http2_discard_host_header", "false");
beginSession();

uint32_t request_idx = 0;
auto request = Http2Frame::makeRequest(Http2Frame::makeClientStreamId(request_idx),
"one.example.com", "/path", {{"host", "two.example.com"}});
sendFrame(request);

waitForNextUpstreamRequest();
EXPECT_EQ(upstream_request_->headers().getHostValue(), "one.example.com,two.example.com");
upstream_request_->encodeHeaders(default_response_headers_, true);
auto frame = readFrame();
EXPECT_EQ(Http2Frame::Type::Headers, frame.type());
EXPECT_EQ(Http2Frame::ResponseStatus::Ok, frame.responseStatus());
tcp_client_->close();
}

// All HTTP/2 static headers must be before non-static headers.
// Verify that codecs validate this.
TEST_P(Http2FrameIntegrationTest, HostBeforeAuthorityIsRejected) {
#ifdef ENVOY_ENABLE_UHV
// TODO(yanavlasov): fix this check for oghttp2 in UHV mode.
if (GetParam().http2_implementation == Http2Impl::Oghttp2) {
return;
}
#endif
beginSession();

Http2Frame request = Http2Frame::makeEmptyHeadersFrame(Http2Frame::makeClientStreamId(0),
Http2Frame::HeadersFlags::EndHeaders);
request.appendStaticHeader(Http2Frame::StaticHeaderIndex::MethodPost);
request.appendStaticHeader(Http2Frame::StaticHeaderIndex::SchemeHttps);
request.appendHeaderWithoutIndexing(Http2Frame::StaticHeaderIndex::Path, "/path");
// Add the `host` header before `:authority`
request.appendHeaderWithoutIndexing({"host", "two.example.com"});
request.appendHeaderWithoutIndexing(Http2Frame::StaticHeaderIndex::Authority, "one.example.com");
request.adjustPayloadSize();

sendFrame(request);

// By default codec treats stream errors as protocol errors and closes the connection.
tcp_client_->waitForDisconnect();
tcp_client_->close();
EXPECT_EQ(1, test_server_->counter("http.config_test.downstream_cx_protocol_error")->value());
}

TEST_P(Http2FrameIntegrationTest, MultipleHeaderOnlyRequests) {
const int kRequestsSentPerIOCycle = 20;
autonomous_upstream_ = true;
Expand Down