Skip to content

Commit 06dba59

Browse files
ext_authz: perform request header checks progressively (#41766)
Commit Message: ext_authz: perform request header checks progressively Additional Description: No behavior change. This pr just makes it so that the ext authz filter checks to make sure header limits (size, count) have not been violated as the mutations are added rather than once at the end. This prevents this filter from hogging the worker thread's time adding a bunch of headers when it will ultimately send a local reply. Risk Level: low Testing: unit tests updated for each case Docs Changes: none necessary Release Notes: none necessary (no externally visible behavior change) --------- Signed-off-by: antoniovleonti <[email protected]>
1 parent 5abbffd commit 06dba59

File tree

3 files changed

+135
-35
lines changed

3 files changed

+135
-35
lines changed

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

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,11 @@ const envoy::extensions::filters::http::ext_authz::v3::CheckSettings& defaultChe
6767
CONSTRUCT_ON_FIRST_USE(envoy::extensions::filters::http::ext_authz::v3::CheckSettings);
6868
}
6969

70+
bool headersWithinLimits(const Http::HeaderMap& headers) {
71+
return headers.size() <= headers.maxHeadersCount() &&
72+
headers.byteSize() <= headers.maxHeadersKb() * 1024;
73+
}
74+
7075
} // namespace
7176

7277
FilterConfig::FilterConfig(const envoy::extensions::filters::http::ext_authz::v3::ExtAuthz& config,
@@ -688,6 +693,11 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
688693
case CheckResult::OK:
689694
ENVOY_STREAM_LOG(trace, "'{}':'{}'", *decoder_callbacks_, key, value);
690695
request_headers_->setCopy(Http::LowerCaseString(key), value);
696+
if (!headersWithinLimits(*request_headers_)) {
697+
stats_.request_header_limits_reached_.inc();
698+
rejectResponse();
699+
return;
700+
}
691701
break;
692702
case CheckResult::IGNORE:
693703
ENVOY_STREAM_LOG(trace, "Ignoring invalid header to set '{}':'{}'.", *decoder_callbacks_,
@@ -707,6 +717,11 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
707717
case CheckResult::OK:
708718
ENVOY_STREAM_LOG(trace, "'{}':'{}'", *decoder_callbacks_, key, value);
709719
request_headers_->addCopy(Http::LowerCaseString(key), value);
720+
if (!headersWithinLimits(*request_headers_)) {
721+
stats_.request_header_limits_reached_.inc();
722+
rejectResponse();
723+
return;
724+
}
710725
break;
711726
case CheckResult::IGNORE:
712727
ENVOY_STREAM_LOG(trace, "Ignoring invalid header to add '{}':'{}'.", *decoder_callbacks_,
@@ -742,6 +757,11 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
742757
// into one entry. The value of that combined entry is separated by ",".
743758
// TODO(dio): Consider to use addCopy instead.
744759
request_headers_->appendCopy(lowercase_key, value);
760+
if (!headersWithinLimits(*request_headers_)) {
761+
stats_.request_header_limits_reached_.inc();
762+
rejectResponse();
763+
return;
764+
}
745765
}
746766
break;
747767
}
@@ -781,6 +801,11 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
781801
case CheckResult::OK:
782802
ENVOY_STREAM_LOG(trace, "'{}'", *decoder_callbacks_, key);
783803
request_headers_->remove(lowercase_key);
804+
if (!headersWithinLimits(*request_headers_)) {
805+
stats_.request_header_limits_reached_.inc();
806+
rejectResponse();
807+
return;
808+
}
784809
break;
785810
case CheckResult::IGNORE:
786811
ENVOY_STREAM_LOG(trace, "Ignoring disallowed header removal '{}'.", *decoder_callbacks_,
@@ -903,12 +928,11 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) {
903928
trace, "ext_authz filter modified query parameter(s), using new path for request: {}",
904929
*decoder_callbacks_, new_path);
905930
request_headers_->setPath(new_path);
906-
}
907-
908-
if (request_headers_->size() > request_headers_->maxHeadersCount() ||
909-
request_headers_->byteSize() > request_headers_->maxHeadersKb() * 1024) {
910-
rejectResponse();
911-
return;
931+
if (!headersWithinLimits(*request_headers_)) {
932+
stats_.request_header_limits_reached_.inc();
933+
rejectResponse();
934+
return;
935+
}
912936
}
913937

914938
if (cluster_) {

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ namespace ExtAuthz {
4747
COUNTER(invalid) \
4848
COUNTER(ignored_dynamic_metadata) \
4949
COUNTER(filter_state_name_collision) \
50-
COUNTER(omitted_response_headers)
50+
COUNTER(omitted_response_headers) \
51+
COUNTER(request_header_limits_reached)
5152

5253
/**
5354
* Wrapper struct for ext_authz filter stats. @see stats_macros.h

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

Lines changed: 103 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2105,17 +2105,47 @@ TEST_F(HttpFilterTest, DeniedResponseWithBodyNotTruncatedWhenLimitIsZero) {
21052105
EXPECT_EQ(1U, config_->stats().denied_.value());
21062106
}
21072107

2108-
// Verifies that the downstream request fails when the ext_authz response
2109-
// would cause the request headers to exceed their limit.
2110-
TEST_F(HttpFilterTest, DownstreamRequestFailsOnHeaderLimit) {
2111-
InSequence s;
2108+
class RequestHeaderLimitTest : public HttpFilterTest {
2109+
public:
2110+
RequestHeaderLimitTest() = default;
21122111

2113-
initialize(R"EOF(
2114-
grpc_service:
2115-
envoy_grpc:
2116-
cluster_name: "ext_authz_server"
2117-
)EOF");
2112+
void runTest(Http::RequestHeaderMap& request_headers,
2113+
Filters::Common::ExtAuthz::Response response) {
2114+
InSequence s;
2115+
2116+
initialize(R"EOF(
2117+
grpc_service:
2118+
envoy_grpc:
2119+
cluster_name: "ext_authz_server"
2120+
)EOF");
2121+
2122+
prepareCheck();
2123+
2124+
EXPECT_CALL(*client_, check(_, _, _, _))
2125+
.WillOnce(Invoke(
2126+
[&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks,
2127+
const envoy::service::auth::v3::CheckRequest&, Tracing::Span&,
2128+
const StreamInfo::StreamInfo&) -> void { request_callbacks_ = &callbacks; }));
2129+
2130+
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
2131+
filter_->decodeHeaders(request_headers, false));
2132+
2133+
// Now the test should fail, since we expect the downstream request to fail.
2134+
EXPECT_CALL(decoder_filter_callbacks_.stream_info_,
2135+
setResponseFlag(Envoy::StreamInfo::CoreResponseFlag::UnauthorizedExternalService));
2136+
EXPECT_CALL(decoder_filter_callbacks_, encodeHeaders_(_, _))
2137+
.WillOnce(Invoke([&](const Http::ResponseHeaderMap& headers, bool) -> void {
2138+
EXPECT_EQ(headers.getStatusValue(),
2139+
std::to_string(enumToInt(Http::Code::InternalServerError)));
2140+
}));
2141+
EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0);
21182142

2143+
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
2144+
EXPECT_EQ(1U, config_->stats().request_header_limits_reached_.value());
2145+
}
2146+
};
2147+
2148+
TEST_F(RequestHeaderLimitTest, HeadersToSetCount) {
21192149
// The total number of headers in the request header map is not allowed to
21202150
// exceed the limit.
21212151
Http::TestRequestHeaderMapImpl request_headers({}, /*max_headers_kb=*/99999,
@@ -2124,32 +2154,77 @@ TEST_F(HttpFilterTest, DownstreamRequestFailsOnHeaderLimit) {
21242154
request_headers.addCopy(Http::Headers::get().Path, "/");
21252155
request_headers.addCopy(Http::Headers::get().Method, "GET");
21262156

2127-
prepareCheck();
2157+
Filters::Common::ExtAuthz::Response response{};
2158+
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
2159+
response.headers_to_set = {{"foo", "bar"}, {"foo2", "bar2"}};
21282160

2129-
EXPECT_CALL(*client_, check(_, _, _, _))
2130-
.WillOnce(
2131-
Invoke([&](Filters::Common::ExtAuthz::RequestCallbacks& callbacks,
2132-
const envoy::service::auth::v3::CheckRequest&, Tracing::Span&,
2133-
const StreamInfo::StreamInfo&) -> void { request_callbacks_ = &callbacks; }));
2161+
runTest(request_headers, response);
2162+
}
21342163

2135-
EXPECT_EQ(Http::FilterHeadersStatus::StopAllIterationAndWatermark,
2136-
filter_->decodeHeaders(request_headers, false));
2164+
TEST_F(RequestHeaderLimitTest, HeadersToSetSize) {
2165+
// The total number of headers in the request header map is not allowed to
2166+
// exceed the limit.
2167+
Http::TestRequestHeaderMapImpl request_headers({}, /*max_headers_kb=*/1,
2168+
/*max_headers_count=*/9999);
2169+
request_headers.addCopy(Http::Headers::get().Host, "host");
2170+
request_headers.addCopy(Http::Headers::get().Path, "/");
2171+
request_headers.addCopy(Http::Headers::get().Method, "GET");
21372172

21382173
Filters::Common::ExtAuthz::Response response{};
21392174
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
2140-
response.headers_to_set = {{"foo", "bar"}, {"foo2", "bar2"}};
2175+
response.headers_to_set = {{"foo", "bar"}, {"foo2", std::string(9999, 'a')}};
21412176

2142-
// Now the test should fail, since we expect the downstream request to fail.
2143-
EXPECT_CALL(decoder_filter_callbacks_.stream_info_,
2144-
setResponseFlag(Envoy::StreamInfo::CoreResponseFlag::UnauthorizedExternalService));
2145-
EXPECT_CALL(decoder_filter_callbacks_, encodeHeaders_(_, _))
2146-
.WillOnce(Invoke([&](const Http::ResponseHeaderMap& headers, bool) -> void {
2147-
EXPECT_EQ(headers.getStatusValue(),
2148-
std::to_string(enumToInt(Http::Code::InternalServerError)));
2149-
}));
2150-
EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()).Times(0);
2177+
runTest(request_headers, response);
2178+
}
21512179

2152-
request_callbacks_->onComplete(std::make_unique<Filters::Common::ExtAuthz::Response>(response));
2180+
// (headers to append can't add new headers, so it won't ever violate the count limit)
2181+
TEST_F(RequestHeaderLimitTest, HeadersToAppendSize) {
2182+
// The total number of headers in the request header map is not allowed to
2183+
// exceed the limit.
2184+
Http::TestRequestHeaderMapImpl request_headers({}, /*max_headers_kb=*/1,
2185+
/*max_headers_count=*/9999);
2186+
request_headers.addCopy(Http::Headers::get().Host, "host");
2187+
request_headers.addCopy(Http::Headers::get().Path, "/");
2188+
request_headers.addCopy(Http::Headers::get().Method, "GET");
2189+
request_headers.addCopy("foo", "original value");
2190+
2191+
Filters::Common::ExtAuthz::Response response{};
2192+
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
2193+
response.headers_to_append = {{"foo", std::string(9999, 'a')}};
2194+
2195+
runTest(request_headers, response);
2196+
}
2197+
2198+
TEST_F(RequestHeaderLimitTest, HeadersToAddCount) {
2199+
// The total number of headers in the request header map is not allowed to
2200+
// exceed the limit.
2201+
Http::TestRequestHeaderMapImpl request_headers({}, /*max_headers_kb=*/99999,
2202+
/*max_headers_count=*/4);
2203+
request_headers.addCopy(Http::Headers::get().Host, "host");
2204+
request_headers.addCopy(Http::Headers::get().Path, "/");
2205+
request_headers.addCopy(Http::Headers::get().Method, "GET");
2206+
2207+
Filters::Common::ExtAuthz::Response response{};
2208+
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
2209+
response.headers_to_add = {{"foo", "bar"}, {"foo2", "bar2"}};
2210+
2211+
runTest(request_headers, response);
2212+
}
2213+
2214+
TEST_F(RequestHeaderLimitTest, HeadersToAddSize) {
2215+
// The total number of headers in the request header map is not allowed to
2216+
// exceed the limit.
2217+
Http::TestRequestHeaderMapImpl request_headers({}, /*max_headers_kb=*/1,
2218+
/*max_headers_count=*/9999);
2219+
request_headers.addCopy(Http::Headers::get().Host, "host");
2220+
request_headers.addCopy(Http::Headers::get().Path, "/");
2221+
request_headers.addCopy(Http::Headers::get().Method, "GET");
2222+
2223+
Filters::Common::ExtAuthz::Response response{};
2224+
response.status = Filters::Common::ExtAuthz::CheckStatus::OK;
2225+
response.headers_to_add = {{"foo2", std::string(9999, 'a')}};
2226+
2227+
runTest(request_headers, response);
21532228
}
21542229

21552230
// Verifies that the downstream request fails when the ext_authz response

0 commit comments

Comments
 (0)