Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Request headers to add #14747

Merged
merged 3 commits into from
Jan 21, 2021
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
2 changes: 1 addition & 1 deletion docs/root/configuration/http/http_conn_man/headers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ Custom request/response headers can be added to a request/response at the weight
route, virtual host, and/or global route configuration level. See the
:ref:`v3 <envoy_v3_api_msg_config.route.v3.RouteConfiguration>` API documentation.

No *:-prefixed* pseudo-header may be modified via this mechanism. The *:path*
Neither *:-prefixed* pseudo-headers nor the Host: header may be modified via this mechanism. The *:path*
and *:authority* headers may instead be modified via mechanisms such as
:ref:`prefix_rewrite <envoy_v3_api_field_config.route.v3.RouteAction.prefix_rewrite>`,
:ref:`regex_rewrite <envoy_v3_api_field_config.route.v3.RouteAction.regex_rewrite>`, and
Expand Down
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Bug Fixes

* active http health checks: properly handles HTTP/2 GOAWAY frames from the upstream. Previously a GOAWAY frame due to a graceful listener drain could cause improper failed health checks due to streams being refused by the upstream on a connection that is going away. To revert to old GOAWAY handling behavior, set the runtime feature `envoy.reloadable_features.health_check.graceful_goaway_handling` to false.
* buffer: tighten network connection read and write buffer high watermarks in preparation to more careful enforcement of read limits. Buffer high-watermark is now set to the exact configured value; previously it was set to value + 1.
* http: disallowing "host:" in request_headers_to_add for behavioral consistency with rejecting :authority header. This behavior can be temporarily reverted by setting `envoy.reloadable_features.treat_host_like_authority` to false.
* http: reverting a behavioral change where upstream connect timeouts were temporarily treated differently from other connection failures. The change back to the original behavior can be temporarily reverted by setting `envoy.reloadable_features.treat_upstream_connect_timeout_as_connect_failure` to false.
* upstream: fix handling of moving endpoints between priorities when active health checks are enabled. Previously moving to a higher numbered priority was a NOOP, and moving to a lower numbered priority caused an abort.

Expand Down
6 changes: 6 additions & 0 deletions source/common/http/header_utility.cc
Original file line number Diff line number Diff line change
Expand Up @@ -314,5 +314,11 @@ bool HeaderUtility::isRemovableHeader(absl::string_view header) {
!absl::EqualsIgnoreCase(header, Headers::get().HostLegacy.get());
}

bool HeaderUtility::isModifiableHeader(absl::string_view header) {
return (header.empty() || header[0] != ':') &&
Copy link
Contributor

Choose a reason for hiding this comment

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

Should this empty check be an ASSERT instead? Seems odd that we'd ever pass an empty string here

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hm, there's a min value check in HeaderValueOption but as I'm not 100% sure validate() is called on every message including a HeaderValueOption (I've been bitten by validate() not being called so not being enforced before) I'd be inclined to leave it as an explicit check just in case.

(!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.treat_host_like_authority") ||
!absl::EqualsIgnoreCase(header, Headers::get().HostLegacy.get()));
}

} // namespace Http
} // namespace Envoy
7 changes: 7 additions & 0 deletions source/common/http/header_utility.h
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ class HeaderUtility {
* may not be removed.
*/
static bool isRemovableHeader(absl::string_view header);

/**
* Returns true if a header may be safely modified without causing additional
* problems. Currently header names beginning with ":" and the "host" header
* may not be modified.
*/
static bool isModifiableHeader(absl::string_view header);
};
} // namespace Http
} // namespace Envoy
6 changes: 4 additions & 2 deletions source/common/router/header_parser.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ HeaderFormatterPtr parseInternal(const envoy::config::core::v3::HeaderValue& hea
// will cause us to have to worry about interaction with other aspects of the
// RouteAction, e.g. prefix rewriting. We also reject other :-prefixed
// headers, since it seems dangerous and there doesn't appear a use case.
if (key[0] == ':') {
throw EnvoyException(":-prefixed headers may not be modified");
// Host is disallowed as it created confusing and inconsistent behaviors for
// HTTP/1 and HTTP/2. It could arguably be allowed on the response path.
if (!Http::HeaderUtility::isModifiableHeader(key)) {
throw EnvoyException(":-prefixed or host headers may not be modified");
}

absl::string_view format(header_value.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 @@ -86,6 +86,7 @@ constexpr const char* runtime_features[] = {
"envoy.reloadable_features.stop_faking_paths",
"envoy.reloadable_features.strict_1xx_and_204_response_headers",
"envoy.reloadable_features.tls_use_io_handle_bio",
"envoy.reloadable_features.treat_host_like_authority",
"envoy.reloadable_features.treat_upstream_connect_timeout_as_connect_failure",
"envoy.reloadable_features.upstream_host_weight_change_causes_rebuild",
"envoy.reloadable_features.vhds_heartbeats",
Expand Down
9 changes: 9 additions & 0 deletions test/common/http/header_utility_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -701,5 +701,14 @@ TEST(RequiredHeaders, IsRemovableHeader) {
EXPECT_TRUE(HeaderUtility::isRemovableHeader("Content-Type"));
}

TEST(RequiredHeaders, IsModifiableHeader) {
EXPECT_FALSE(HeaderUtility::isRemovableHeader(":path"));
EXPECT_FALSE(HeaderUtility::isRemovableHeader("host"));
EXPECT_FALSE(HeaderUtility::isRemovableHeader("Host"));
EXPECT_TRUE(HeaderUtility::isRemovableHeader(""));
EXPECT_TRUE(HeaderUtility::isRemovableHeader("hostname"));
EXPECT_TRUE(HeaderUtility::isRemovableHeader("Content-Type"));
}

} // namespace Http
} // namespace Envoy
31 changes: 27 additions & 4 deletions test/common/router/config_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1721,10 +1721,10 @@ most_specific_header_mutations_wins: true
}
}

// Validate that we can't add :-prefixed request headers.
TEST_F(RouteMatcherTest, TestRequestHeadersToAddNoPseudoHeader) {
// Validate that we can't add :-prefixed or Host request headers.
TEST_F(RouteMatcherTest, TestRequestHeadersToAddNoHostOrPseudoHeader) {
for (const std::string& header :
{":path", ":authority", ":method", ":scheme", ":status", ":protocol"}) {
{":path", ":authority", ":method", ":scheme", ":status", ":protocol", "host"}) {
const std::string yaml = fmt::format(R"EOF(
virtual_hosts:
- name: www2
Expand All @@ -1743,10 +1743,33 @@ TEST_F(RouteMatcherTest, TestRequestHeadersToAddNoPseudoHeader) {
parseRouteConfigurationFromYaml(yaml);

EXPECT_THROW_WITH_MESSAGE(TestConfigImpl config(route_config, factory_context_, true),
EnvoyException, ":-prefixed headers may not be modified");
EnvoyException, ":-prefixed or host headers may not be modified");
}
}

TEST_F(RouteMatcherTest, TestRequestHeadersToAddLegacyHostHeader) {
TestScopedRuntime scoped_runtime;
Runtime::LoaderSingleton::getExisting()->mergeValues(
{{"envoy.reloadable_features.treat_host_like_authority", "false"}});

const std::string yaml = R"EOF(
virtual_hosts:
- name: www2
domains: ["*"]
request_headers_to_add:
- header:
key: "host"
value: vhost-www2
append: false
)EOF";

NiceMock<Envoy::StreamInfo::MockStreamInfo> stream_info;

envoy::config::route::v3::RouteConfiguration route_config = parseRouteConfigurationFromYaml(yaml);

EXPECT_NO_THROW(TestConfigImpl config(route_config, factory_context_, true));
}

// Validate that we can't remove :-prefixed request headers.
TEST_F(RouteMatcherTest, TestRequestHeadersToRemoveNoPseudoHeader) {
for (const std::string& header :
Expand Down