Skip to content

Commit eef470f

Browse files
ext_authz: send local reply on failed response header mutations (#41844)
Commit Message: ext_authz: send local reply on failed response header mutations Additional Description: Without this PR, the ext authz filter will silently (to the downstream client) fail to add headers if it would violate the response header map limits. This is bad because the app being proxied could rely on the headers added by ext authz. Instead it is better if the ext authz filter just sent a local reply the same way ext_proc does. This way it will be clear that something went wrong. If this behavior is undesirable, the filter can always be configured with these check disabled. Since we are now sending a local reply instead of an incomplete reply, we can now do the simpler thing and just add headers and do a post-check, which means we can also using the existing headersWithinLimits function. Risk Level: low Testing: unit tested and integration tests updated Docs Changes: config comments updated Release Notes: changelog updated --------- Signed-off-by: antoniovleonti <[email protected]> Signed-off-by: Antonio V. Leonti <[email protected]>
1 parent 89fe56f commit eef470f

File tree

7 files changed

+132
-241
lines changed

7 files changed

+132
-241
lines changed

api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -324,8 +324,7 @@ message ExtAuthz {
324324
uint32 max_denied_response_body_bytes = 30;
325325

326326
// When set to ``true``, the filter will enforce the response header map's count and size limits
327-
// by dropping headers added by the external auth service. Enabling this may break applications
328-
// relying on those headers!
327+
// by sending a local reply when those limits are violated.
329328
//
330329
// When set to ``false``, the filter will ignore the response header map's limits and add / set
331330
// all response headers as specified by the external auth service.

changelogs/current.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,8 +55,8 @@ minor_behavior_changes:
5555
``envoy.reloadable_features.ext_proc_stream_close_optimization`` to ``false``.
5656
- area: ext_authz
5757
change: |
58-
Check that the response header count is less than the configured limits before applying mutations, and do not
59-
add new headers if not.
58+
Check that the response header count and size is less than the configured limits after applying
59+
mutations and send a local reply if not.
6060
- area: ext_authz
6161
change: |
6262
Fixed HTTP ext_authz service to properly propagate headers (such as ``set-cookie``) back to clients. The

docs/root/configuration/http/http_filters/ext_authz_filter.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ The HTTP filter outputs statistics in the ``cluster.<route target cluster>.ext_a
153153
invalid, Counter, Total responses rejected due to invalid header or query parameter mutations.
154154
omitted_response_headers, Counter, "Total responses for which ext_authz rejected any number of
155155
headers due to the header map constraints."
156+
request_header_limits_reached, Counter, "Total requests for which ext_authz sent a local reply
157+
because it couldn't apply all header mutations"
158+
response_header_limits_reached, Counter, "Total responses for which ext_authz sent a local reply
159+
because it couldn't apply all header mutations"
156160

157161
Dynamic Metadata
158162
----------------

source/extensions/filters/http/ext_authz/ext_authz.cc

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -488,47 +488,43 @@ Http::FilterHeadersStatus Filter::encodeHeaders(Http::ResponseHeaderMap& headers
488488
"set to the encoded response:",
489489
*encoder_callbacks_, response_headers_to_add_.size(),
490490
response_headers_to_set_.size());
491-
bool omitted_response_headers = false;
492491
if (!response_headers_to_add_.empty()) {
493492
ENVOY_STREAM_LOG(
494493
trace, "ext_authz filter added header(s) to the encoded response:", *encoder_callbacks_);
495494
for (const auto& [key, value] : response_headers_to_add_) {
496-
if (config_->enforceResponseHeaderLimits() && headers.size() >= headers.maxHeadersCount()) {
497-
omitted_response_headers = true;
498-
break;
499-
}
500495
ENVOY_STREAM_LOG(trace, "'{}':'{}'", *encoder_callbacks_, key.get(), value);
501496
headers.addCopy(key, value);
497+
if (config_->enforceResponseHeaderLimits() && !headersWithinLimits(headers)) {
498+
responseHeaderLimitsReached();
499+
return Http::FilterHeadersStatus::StopIteration;
500+
}
502501
}
503502
}
504503

505504
if (!response_headers_to_set_.empty()) {
506505
ENVOY_STREAM_LOG(
507506
trace, "ext_authz filter set header(s) to the encoded response:", *encoder_callbacks_);
508507
for (const auto& [key, value] : response_headers_to_set_) {
509-
if (config_->enforceResponseHeaderLimits() && headers.get(key).empty() &&
510-
headers.size() >= headers.maxHeadersCount()) {
511-
omitted_response_headers = true;
512-
// Continue because there could be other existing headers that can be set without increasing
513-
// the number of headers.
514-
continue;
515-
}
516508
ENVOY_STREAM_LOG(trace, "'{}':'{}'", *encoder_callbacks_, key.get(), value);
517509
headers.setCopy(key, value);
510+
if (config_->enforceResponseHeaderLimits() && !headersWithinLimits(headers)) {
511+
responseHeaderLimitsReached();
512+
return Http::FilterHeadersStatus::StopIteration;
513+
}
518514
}
519515
}
520516

521517
if (!response_headers_to_add_if_absent_.empty()) {
522518
for (const auto& [key, value] : response_headers_to_add_if_absent_) {
523519
ENVOY_STREAM_LOG(trace, "'{}':'{}'", *encoder_callbacks_, key.get(), value);
524520
if (auto header_entry = headers.get(key); header_entry.empty()) {
525-
if (config_->enforceResponseHeaderLimits() && headers.size() >= headers.maxHeadersCount()) {
526-
omitted_response_headers = true;
527-
break;
528-
}
529521
ENVOY_STREAM_LOG(trace, "ext_authz filter added header(s) to the encoded response:",
530522
*encoder_callbacks_);
531523
headers.addCopy(key, value);
524+
if (config_->enforceResponseHeaderLimits() && !headersWithinLimits(headers)) {
525+
responseHeaderLimitsReached();
526+
return Http::FilterHeadersStatus::StopIteration;
527+
}
532528
}
533529
}
534530
}
@@ -540,16 +536,14 @@ Http::FilterHeadersStatus Filter::encodeHeaders(Http::ResponseHeaderMap& headers
540536
ENVOY_STREAM_LOG(
541537
trace, "ext_authz filter set header(s) to the encoded response:", *encoder_callbacks_);
542538
headers.setCopy(key, value);
539+
if (config_->enforceResponseHeaderLimits() && !headersWithinLimits(headers)) {
540+
responseHeaderLimitsReached();
541+
return Http::FilterHeadersStatus::StopIteration;
542+
}
543543
}
544544
}
545545
}
546546

547-
if (omitted_response_headers) {
548-
ENVOY_LOG_EVERY_POW_2(
549-
warn, "Some ext_authz response headers weren't added because the header map was full.");
550-
stats_.omitted_response_headers_.inc();
551-
}
552-
553547
return Http::FilterHeadersStatus::Continue;
554548
}
555549

@@ -1052,6 +1046,20 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
10521046
}
10531047
}
10541048

1049+
void Filter::responseHeaderLimitsReached() {
1050+
const Http::Code status = Http::Code::InternalServerError;
1051+
ENVOY_LOG_EVERY_POW_2(warn,
1052+
"ext_authz filter couldn't add all response header mutations. "
1053+
"Sending local reply with response status code: {}",
1054+
enumToInt(status));
1055+
stats_.response_header_limits_reached_.inc();
1056+
encoder_callbacks_->streamInfo().setResponseFlag(
1057+
StreamInfo::CoreResponseFlag::UnauthorizedExternalService);
1058+
encoder_callbacks_->sendLocalReply(
1059+
status, EMPTY_STRING, nullptr, absl::nullopt,
1060+
Filters::Common::ExtAuthz::ResponseCodeDetails::get().AuthzInvalid);
1061+
}
1062+
10551063
void Filter::rejectResponse() {
10561064
const Http::Code status = Http::Code::InternalServerError;
10571065
ENVOY_STREAM_LOG(trace, "ext_authz filter invalidated the response. Response status code: {}",

source/extensions/filters/http/ext_authz/ext_authz.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ namespace ExtAuthz {
4848
COUNTER(ignored_dynamic_metadata) \
4949
COUNTER(filter_state_name_collision) \
5050
COUNTER(omitted_response_headers) \
51-
COUNTER(request_header_limits_reached)
51+
COUNTER(request_header_limits_reached) \
52+
COUNTER(response_header_limits_reached)
5253

5354
/**
5455
* Wrapper struct for ext_authz filter stats. @see stats_macros.h
@@ -425,6 +426,8 @@ class Filter : public Logger::Loggable<Logger::Id::ext_authz>,
425426
validateAndCheckDecoderHeaderMutation(Filters::Common::MutationRules::CheckOperation operation,
426427
absl::string_view key, absl::string_view value) const;
427428

429+
void responseHeaderLimitsReached();
430+
428431
// Called when the filter is configured to reject invalid responses & the authz response contains
429432
// invalid header or query parameters. Sends a local response with the configured rejection status
430433
// code.

test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,12 +2131,7 @@ TEST_P(ExtAuthzGrpcIntegrationTest, EncodeHeadersToAddExceedsLimit) {
21312131

21322132
ASSERT_TRUE(response_->waitForEndStream());
21332133
EXPECT_TRUE(response_->complete());
2134-
EXPECT_EQ("200", response_->headers().getStatusValue());
2135-
// NB: Something else adds headers to the response between the ext_authz filter and the downstream
2136-
// client so the header count is greater than you might expect (100), but we can at least be sure
2137-
// that all the ext_authz response headers didn't get added.
2138-
EXPECT_LT(response_->headers().size(), 120);
2139-
EXPECT_TRUE(response_->headers().get(Http::LowerCaseString("new-header-99")).empty());
2134+
EXPECT_EQ("500", response_->headers().getStatusValue());
21402135
cleanup();
21412136
}
21422137

@@ -2180,16 +2175,7 @@ TEST_P(ExtAuthzGrpcIntegrationTest, EncodeHeadersToSetExceedsLimit) {
21802175
ASSERT_TRUE(response_->waitForEndStream());
21812176

21822177
EXPECT_TRUE(response_->complete());
2183-
EXPECT_EQ("200", response_->headers().getStatusValue());
2184-
2185-
EXPECT_LT(response_->headers().size(), 120);
2186-
// The last new headers shouldn't get added to the header map, but the
2187-
// existing upstream header will be overridden.
2188-
EXPECT_TRUE(response_->headers().get(Http::LowerCaseString("new-header-99")).empty());
2189-
EXPECT_EQ("new-value", response_->headers()
2190-
.get(Http::LowerCaseString("upstream-header"))[0]
2191-
->value()
2192-
.getStringView());
2178+
EXPECT_EQ("500", response_->headers().getStatusValue());
21932179
cleanup();
21942180
}
21952181

@@ -2231,10 +2217,7 @@ TEST_P(ExtAuthzGrpcIntegrationTest, EncodeHeadersToAppendIfAbsentExceedsLimit) {
22312217
ASSERT_TRUE(response_->waitForEndStream());
22322218

22332219
EXPECT_TRUE(response_->complete());
2234-
EXPECT_EQ("200", response_->headers().getStatusValue());
2235-
2236-
EXPECT_LT(response_->headers().size(), 120);
2237-
EXPECT_TRUE(response_->headers().get(Http::LowerCaseString("new-header-99")).empty());
2220+
EXPECT_EQ("500", response_->headers().getStatusValue());
22382221
cleanup();
22392222
}
22402223

0 commit comments

Comments
 (0)