-
Notifications
You must be signed in to change notification settings - Fork 5.5k
router: add config to reject requests with invalid envoy headers #7323
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
Changes from 6 commits
cc0d89a
9104968
491cd96
072639d
02382e9
ed2a492
6d381c3
79ecd36
6eec18c
9bc4cfe
a0f8331
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,7 +191,8 @@ message ResponseFlagFilter { | |
| "RLSE", | ||
| "DC", | ||
| "URX", | ||
| "SI" | ||
| "SI", | ||
| "IH" | ||
| ] | ||
| }]; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -191,6 +191,9 @@ message ResponseFlags { | |
| REASON_UNSPECIFIED = 0; | ||
| // The request was denied by the external authorization service. | ||
| EXTERNAL_SERVICE = 1; | ||
| // The request was denied because a header value failed a :ref:`strict header check | ||
| // <envoy_api_field_config.filter.http.router.v2.Router.strict_check_headers>` failed. | ||
| STRICT_HEADER_CHECK_FAILED = 2; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure if this belongs here, this seems to be dealing with authorization of an endpoint, not about whether the request is valid. If there isn't a similar flag for "invalid request reason" we can probably just drop this
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you're right. It looks like I should instead move this to be a |
||
| } | ||
|
|
||
| Reason reason = 1; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ Version history | |
| * access log: added a new field for route name to file and gRPC access logger. | ||
| * access log: added a new field for response code details in :ref:`file access logger<config_access_log_format_response_code_details>` and :ref:`gRPC access logger<envoy_api_field_data.accesslog.v2.HTTPResponseProperties.response_code_details>`. | ||
| * access log: added several new variables for exposing information about the downstream TLS connection to :ref:`file access logger<config_access_log_format_response_code_details>` and :ref:`gRPC access logger<envoy_api_field_data.accesslog.v2.AccessLogCommon.tls_properties>`. | ||
| * access log: added a new flag for request rejected due to failed strict header check | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: end with period |
||
| * admin: the administration interface now includes a :ref:`/ready endpoint <operations_admin_interface>` for easier readiness checks. | ||
| * admin: extend :ref:`/runtime_modify endpoint <operations_admin_interface_runtime_modify>` to support parameters within the request body. | ||
| * admin: the :ref:`/listener endpoint <operations_admin_interface_listeners>` now returns :ref:`listeners.proto<envoy_api_msg_admin.v2alpha.Listeners>` which includes listener names and ports. | ||
|
|
@@ -59,6 +60,8 @@ Version history | |
| * router: added :ref:`RouteAction's auto_host_rewrite_header <envoy_api_field_route.RouteAction.auto_host_rewrite_header>` to allow upstream host header substitution with some other header's value | ||
| * router: added support for UPSTREAM_REMOTE_ADDRESS :ref:`header formatter | ||
| <config_http_conn_man_headers_custom_request_headers>`. | ||
| * router: add ability to reject a request that includes invalid values for | ||
| headers configured in :ref:`strict_check_headers <envoy_api_field_config.filter.http.router.v2.Router.strict_check_headers>` | ||
| * runtime: added support for :ref:`flexible layering configuration | ||
| <envoy_api_field_config.bootstrap.v2.Bootstrap.layered_runtime>`. | ||
| * runtime: added support for statically :ref:`specifying the runtime in the bootstrap configuration | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -81,6 +81,9 @@ class RetryStateImpl : public RetryState { | |
| bool wouldRetryFromReset(const Http::StreamResetReason reset_reason); | ||
| RetryStatus shouldRetry(bool would_retry, DoRetryCallback callback); | ||
|
|
||
| static uint32_t parseRetryOn_(absl::string_view config, bool flag_parse_failures); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these used for anything anymore?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops good catch. Removed. |
||
| static uint32_t parseRetryGrpcOn_(absl::string_view config, bool flag_parse_failures); | ||
|
|
||
| const Upstream::ClusterInfo& cluster_; | ||
| Runtime::Loader& runtime_; | ||
| Runtime::RandomGenerator& random_; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -222,6 +222,29 @@ Filter::~Filter() { | |
| ASSERT(!retry_state_); | ||
| } | ||
|
|
||
| const FilterUtility::StrictHeaderChecker::HeaderCheckResult | ||
| FilterUtility::StrictHeaderChecker::test(Http::HeaderMap& headers, | ||
| const Http::LowerCaseString& target_header) { | ||
| HeaderCheckResult r; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This doesn't seem to be used besides returning? Might be easier to just inline it e.g.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good catch; this is was from a time when we always returned "valid" to ignore a |
||
| if (target_header == Http::Headers::get().EnvoyUpstreamRequestTimeoutMs) { | ||
| return isInteger(headers.EnvoyUpstreamRequestTimeoutMs()); | ||
| } else if (target_header == Http::Headers::get().EnvoyUpstreamRequestPerTryTimeoutMs) { | ||
| return isInteger(headers.EnvoyUpstreamRequestPerTryTimeoutMs()); | ||
| } else if (target_header == Http::Headers::get().EnvoyMaxRetries) { | ||
| return isInteger(headers.EnvoyMaxRetries()); | ||
| } else if (target_header == Http::Headers::get().EnvoyRetryOn) { | ||
| return hasValidRetryFields(headers.EnvoyRetryOn(), &Router::RetryStateImpl::parseRetryOn); | ||
| } else if (target_header == Http::Headers::get().EnvoyRetryGrpcOn) { | ||
| return hasValidRetryFields(headers.EnvoyRetryGrpcOn(), | ||
| &Router::RetryStateImpl::parseRetryGrpcOn); | ||
| } else { | ||
| // Should only validate headers for which we have implemented a validator | ||
| ASSERT(false); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. you can use |
||
| } | ||
|
xyu-stripe marked this conversation as resolved.
|
||
|
|
||
| return r; | ||
| } | ||
|
|
||
| Stats::StatName Filter::upstreamZone(Upstream::HostDescriptionConstSharedPtr upstream_host) { | ||
| return upstream_host ? upstream_host->localityZoneStatName() : config_.empty_stat_name_; | ||
| } | ||
|
|
@@ -381,6 +404,24 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::HeaderMap& headers, bool e | |
| ENVOY_STREAM_LOG(debug, "cluster '{}' match for URL '{}'", *callbacks_, | ||
| route_entry_->clusterName(), headers.Path()->value().getStringView()); | ||
|
|
||
| if (config_.strict_check_headers_ != nullptr && !config_.strict_check_headers_->empty()) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you can omit the empty check since the for loop will just noop in that case |
||
| for (const auto& header : *config_.strict_check_headers_) { | ||
| const auto res = FilterUtility::StrictHeaderChecker::test(headers, header); | ||
| if (!res.valid_) { | ||
| callbacks_->streamInfo().setResponseFlag( | ||
| StreamInfo::ResponseFlag::InvalidEnvoyRequestHeaders); | ||
| const std::string body = fmt::format("invalid header '{}' with value '{}'", | ||
| std::string(res.entry_->key().getStringView()), | ||
| std::string(res.entry_->value().getStringView())); | ||
| const std::string details = | ||
| absl::StrCat(StreamInfo::ResponseCodeDetails::get().InvalidEnvoyRequestHeaders, "{", | ||
| res.entry_->key().getStringView(), "}"); | ||
| callbacks_->sendLocalReply(Http::Code::BadRequest, body, nullptr, absl::nullopt, details); | ||
| return Http::FilterHeadersStatus::StopIteration; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| const Http::HeaderEntry* request_alt_name = headers.EnvoyUpstreamAltStatName(); | ||
| if (request_alt_name) { | ||
| // TODO(#7003): converting this header value into a StatName requires | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,6 +67,41 @@ class FilterUtility { | |
| bool hedge_on_per_try_timeout_; | ||
| }; | ||
|
|
||
| class StrictHeaderChecker { | ||
| public: | ||
| struct HeaderCheckResult { | ||
| bool valid_ = true; | ||
| const Http::HeaderEntry* entry_; | ||
| }; | ||
|
|
||
| static const HeaderCheckResult test(Http::HeaderMap& headers, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mind adding a comment to this explaining what it does? A more descriptive name might be useful too, maybe |
||
| const Http::LowerCaseString& target_header); | ||
|
|
||
| using ParseRetryFlagsFunc = std::function<uint32_t(absl::string_view)>; | ||
|
|
||
| private: | ||
| static HeaderCheckResult hasValidRetryFields(Http::HeaderEntry* header_entry, | ||
| const ParseRetryFlagsFunc& parseFn) { | ||
| HeaderCheckResult r; | ||
| if (header_entry) { | ||
| const auto flags = parseFn(header_entry->value().getStringView()); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thinking about this some more, maybe it would make more sense to have the parseFn return Sorry for the churn here, hopefully we can get this to a place where we're all happy with it
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No worries re: churn. I think a named struct would be clearer at calling sites, but going to repush with |
||
| r.valid_ = (flags & RetryPolicy::RETRY_UNKNOWN_FIELD_ERROR) == 0; | ||
| r.entry_ = header_entry; | ||
| } | ||
| return r; | ||
| } | ||
|
|
||
| static HeaderCheckResult isInteger(Http::HeaderEntry* header_entry) { | ||
| HeaderCheckResult r; | ||
| if (header_entry) { | ||
| uint64_t out; | ||
| r.valid_ = absl::SimpleAtoi(header_entry->value().getStringView(), &out); | ||
| r.entry_ = header_entry; | ||
| } | ||
| return r; | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * Set the :scheme header based on the properties of the upstream cluster. | ||
| */ | ||
|
|
@@ -115,6 +150,7 @@ class FilterConfig { | |
| Stats::Scope& scope, Upstream::ClusterManager& cm, Runtime::Loader& runtime, | ||
| Runtime::RandomGenerator& random, ShadowWriterPtr&& shadow_writer, | ||
| bool emit_dynamic_stats, bool start_child_span, bool suppress_envoy_headers, | ||
| const Protobuf::RepeatedPtrField<std::string>& strict_check_headers, | ||
| TimeSource& time_source, Http::Context& http_context) | ||
| : scope_(scope), local_info_(local_info), cm_(cm), runtime_(runtime), | ||
| random_(random), stats_{ALL_ROUTER_STATS(POOL_COUNTER_PREFIX(scope, stat_prefix))}, | ||
|
|
@@ -123,7 +159,14 @@ class FilterConfig { | |
| stat_name_pool_(scope_.symbolTable()), retry_(stat_name_pool_.add("retry")), | ||
| zone_name_(stat_name_pool_.add(local_info_.zoneName())), | ||
| empty_stat_name_(stat_name_pool_.add("")), shadow_writer_(std::move(shadow_writer)), | ||
| time_source_(time_source) {} | ||
| time_source_(time_source) { | ||
| if (!strict_check_headers.empty()) { | ||
| strict_check_headers_ = std::make_unique<HeaderVector>(); | ||
| for (const auto& header : strict_check_headers) { | ||
| strict_check_headers_->emplace_back(Http::LowerCaseString(header)); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| FilterConfig(const std::string& stat_prefix, Server::Configuration::FactoryContext& context, | ||
| ShadowWriterPtr&& shadow_writer, | ||
|
|
@@ -132,11 +175,14 @@ class FilterConfig { | |
| context.runtime(), context.random(), std::move(shadow_writer), | ||
| PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, dynamic_stats, true), | ||
| config.start_child_span(), config.suppress_envoy_headers(), | ||
| context.api().timeSource(), context.httpContext()) { | ||
| config.strict_check_headers(), context.api().timeSource(), | ||
| context.httpContext()) { | ||
| for (const auto& upstream_log : config.upstream_log()) { | ||
| upstream_logs_.push_back(AccessLog::AccessLogFactory::fromProto(upstream_log, context)); | ||
| } | ||
| } | ||
| using HeaderVector = std::vector<Http::LowerCaseString>; | ||
| using StrictCheckHeadersPtr = std::unique_ptr<HeaderVector>; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. since this is just a unique ptr to HeaderVector, i'd probably call it just |
||
|
|
||
| ShadowWriter& shadowWriter() { return *shadow_writer_; } | ||
| TimeSource& timeSource() { return time_source_; } | ||
|
|
@@ -150,6 +196,8 @@ class FilterConfig { | |
| const bool emit_dynamic_stats_; | ||
| const bool start_child_span_; | ||
| const bool suppress_envoy_headers_; | ||
| // TODO(xyu-stripe): Make this a bitset to keep cluster memory footprint down | ||
| StrictCheckHeadersPtr strict_check_headers_; | ||
| std::list<AccessLog::InstanceSharedPtr> upstream_logs_; | ||
| Http::Context& http_context_; | ||
| Stats::StatNamePool stat_name_pool_; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this change needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added this because we want to validate values set for
strict_check_headersin the config in api/envoy/config/filter/http/router/v2/router.proto#L14, which requires inclusion ofcom_envoyproxy_protoc_gen_validate/validate/validate.proto.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm no bazel expert but I thought this was only necessary if you directly referenced the
api_go_proto_libraryrule in this BUILD file which it doesn't seem like you're doing? I could be totally wrong here, so feel free to leave it if it's necessary for the buildThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will repush with this removed and add it back if it turns out to actually result in failed builds.
Hmm.. I rebuilt
//source/exe:envoy-static(not from a clean build though) with this change removed and things seemed to work. There are other places inapi/envoy/config/filter/http/**that reference proto validation without this declaration in theirBUILDfiles.