Skip to content

Commit

Permalink
CVE-2022-21655
Browse files Browse the repository at this point in the history
Crash with direct_response

Signed-off-by: Yan Avlasov <[email protected]>
  • Loading branch information
yanavlasov authored and RyanTheOptimist committed Feb 22, 2022
1 parent e9f936d commit 177d608
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 4 deletions.
1 change: 1 addition & 0 deletions docs/root/version_history/current.rst
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Bug Fixes
*Changes expected to improve the state of the world and are unlikely to have negative effects*

* access_log: fix memory leak when reopening an access log fails. Access logs will now try to be reopened on each subsequent flush attempt after a failure.
* data plane: fix crash when internal redirect selects a route configured with direct response or redirect actions.
* data plane: fixing error handling where writing to a socket failed while under the stack of processing. This should genreally affect HTTP/3. This behavioral change can be reverted by setting ``envoy.reloadable_features.allow_upstream_inline_write`` to false.
* eds: fix the eds cluster update by allowing update on the locality of the cluster endpoints. This behavioral change can be temporarily reverted by setting runtime guard ``envoy.reloadable_features.support_locality_update_on_eds_cluster_endpoints`` to false.
* jwt_authn: fixed the crash when a CONNECT request is sent to JWT filter configured with regex match on the Host header.
Expand Down
3 changes: 2 additions & 1 deletion source/common/router/router.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1692,7 +1692,8 @@ bool Filter::convertRequestHeadersForInternalRedirect(Http::RequestHeaderMap& do
return false;
}

const auto& route_name = route->routeEntry()->routeName();
const auto& route_name = route->directResponseEntry() ? route->directResponseEntry()->routeName()
: route->routeEntry()->routeName();
for (const auto& predicate : policy.predicates()) {
if (!predicate->acceptTargetRoute(*filter_state, route_name, !scheme_is_http,
!target_is_http)) {
Expand Down
33 changes: 30 additions & 3 deletions test/common/router/router_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ using testing::AtLeast;
using testing::Eq;
using testing::InSequence;
using testing::Invoke;
using testing::InvokeWithoutArgs;
using testing::MockFunction;
using testing::NiceMock;
using testing::Property;
Expand Down Expand Up @@ -319,7 +320,7 @@ TEST_F(RouterTest, MissingRequiredHeaders) {
sendLocalReply(Http::Code::ServiceUnavailable,
testing::Eq("missing required header: :method"), _, _,
"filter_removed_required_request_headers{missing_required_header:_:method}"))
.WillOnce(testing::InvokeWithoutArgs([] {}));
.WillOnce(InvokeWithoutArgs([] {}));
router_.decodeHeaders(headers, true);
router_.onDestroy();
}
Expand Down Expand Up @@ -3411,8 +3412,7 @@ TEST_F(RouterTest, RetryUpstreamResetResponseStarted) {
// Normally, sendLocalReply will actually send the reply, but in this case the
// HCM will detect the headers have already been sent and not route through
// the encoder again.
EXPECT_CALL(callbacks_, sendLocalReply(_, _, _, _, _)).WillOnce(testing::InvokeWithoutArgs([] {
}));
EXPECT_CALL(callbacks_, sendLocalReply(_, _, _, _, _)).WillOnce(InvokeWithoutArgs([] {}));
encoder1.stream_.resetStream(Http::StreamResetReason::RemoteReset);
// For normal HTTP, once we have a 200 we consider this a success, even if a
// later reset occurs.
Expand Down Expand Up @@ -4207,6 +4207,33 @@ TEST_F(RouterTest, HttpInternalRedirectSucceeded) {
->value());
}

TEST_F(RouterTest, HttpInternalRedirectMatchedToDirectResponseSucceeded) {
NiceMock<MockDirectResponseEntry> direct_response;
std::string route_name("route-test-name");
EXPECT_CALL(direct_response, routeName()).WillOnce(ReturnRef(route_name));

enableRedirects();
sendRequest();
EXPECT_CALL(callbacks_, clearRouteCache()).WillOnce(InvokeWithoutArgs([&]() -> void {
// Direct message route should be matched after internal redirect
EXPECT_CALL(*callbacks_.route_, routeEntry()).WillRepeatedly(Return(nullptr));
EXPECT_CALL(*callbacks_.route_, directResponseEntry()).WillRepeatedly(Return(&direct_response));
}));
EXPECT_CALL(callbacks_, recreateStream(_)).WillOnce(Return(true));

response_decoder_->decodeHeaders(std::move(redirect_headers_), false);
EXPECT_EQ(1U, cm_.thread_local_cluster_.cluster_.info_->stats_store_
.counter("upstream_internal_redirect_succeeded_total")
.value());

// In production, the HCM recreateStream would have called this.
router_.onDestroy();
EXPECT_EQ(1, callbacks_.streamInfo()
.filterState()
->getDataMutable<StreamInfo::UInt32Accessor>("num_internal_redirects")
->value());
}

TEST_F(RouterTest, InternalRedirectStripsFragment) {
enableRedirects();
default_request_headers_.setForwardedProto("http");
Expand Down
42 changes: 42 additions & 0 deletions test/integration/redirect_integration_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ class RedirectIntegrationTest : public HttpProtocolIntegrationTest {
->set_value(3);
config_helper_.addVirtualHost(handle_max_3_hop);

auto handle_by_direct_response = config_helper_.createVirtualHost("handle.direct.response");
handle_by_direct_response.mutable_routes(0)->set_name("direct_response");
handle_by_direct_response.mutable_routes(0)->mutable_direct_response()->set_status(204);
handle_by_direct_response.mutable_routes(0)
->mutable_direct_response()
->mutable_body()
->set_inline_string(EMPTY_STRING);
config_helper_.addVirtualHost(handle_by_direct_response);

HttpProtocolIntegrationTest::initialize();
}

Expand Down Expand Up @@ -665,6 +674,39 @@ TEST_P(RedirectIntegrationTest, InvalidRedirect) {
response->headers().get(test_header_key_)[0]->value().getStringView());
}

TEST_P(RedirectIntegrationTest, InternalRedirectHandledByDirectResponse) {
useAccessLog("%RESPONSE_FLAGS% %RESPONSE_CODE% %RESPONSE_CODE_DETAILS% %RESP(test-header)%");
// Validate that header sanitization is only called once.
config_helper_.addConfigModifier(
[](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager&
hcm) { hcm.set_via("via_value"); });
initialize();

codec_client_ = makeHttpConnection(lookupPort("http"));

default_request_headers_.setHost("handle.internal.redirect");
IntegrationStreamDecoderPtr response =
codec_client_->makeHeaderOnlyRequest(default_request_headers_);

waitForNextUpstreamRequest();

redirect_response_.setLocation("http://handle.direct.response/");
upstream_request_->encodeHeaders(redirect_response_, true);

ASSERT_TRUE(response->waitForEndStream());
ASSERT_TRUE(response->complete());
EXPECT_EQ("204", response->headers().getStatusValue());
EXPECT_EQ(1, test_server_->counter("cluster.cluster_0.upstream_internal_redirect_succeeded_total")
->value());
// 302 was never returned downstream
EXPECT_EQ(0, test_server_->counter("http.config_test.downstream_rq_3xx")->value());
EXPECT_EQ(1, test_server_->counter("http.config_test.downstream_rq_2xx")->value());
EXPECT_THAT(waitForAccessLog(access_log_name_, 0),
HasSubstr("302 internal_redirect test-header-value\n"));
// No test header
EXPECT_THAT(waitForAccessLog(access_log_name_, 1), HasSubstr("204 direct_response -\n"));
}

INSTANTIATE_TEST_SUITE_P(Protocols, RedirectIntegrationTest,
testing::ValuesIn(HttpProtocolIntegrationTest::getProtocolTestParams()),
HttpProtocolIntegrationTest::protocolTestParamsToString);
Expand Down

0 comments on commit 177d608

Please sign in to comment.