Chrome · Network
CVE-2026-85043
Logic Error in Network
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/http/http_auth_controller.cc |
modified | |
NetLogWithSourcenet/http/http_auth_controller.h |
modified | |
SSLInfonet/http/http_auth_controller.h |
modified | |
X509Certificatenet/http/http_auth_controller.h |
modified |
Files Changed
net/http/http_auth_controller.ccnet/http/http_auth_controller.hnet/http/http_auth_controller_unittest.cc
Patch
From 65ac1eb902617bd487b9b0de4b4b3ba69db9c17c Mon Sep 17 00:00:00 2001 From: Sebastien Lalancette <[email protected]> Date: Fri, 28 Aug 2026 09:01:59 -0700 Subject: [PATCH] Reset connection-based auth handlers on certificate change Connection-based HTTP authentication schemes derive per-connection state from the server certificate presented when the handler is created. If a subsequent authentication challenge is received over a connection presenting a different server certificate, the existing handler is no longer valid for that connection. This change updates HttpAuthController to track the server certificate associated with the active connection-based handler and drop the handler if a subsequent challenge arrives over a connection with a different certificate. Fixed: 533502257 Change-Id: I8bda23f9d03bef3366a798cbed5d6d2caf3c72c1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8284608 Commit-Queue: Sebastien Lalancette <[email protected]> Reviewed-by: Kenichi Ishibashi <[email protected]> Cr-Commit-Position: refs/heads/main@{#1688069} --- diff --git a/net/http/http_auth_controller.cc b/net/http/http_auth_controller.cc index 667c0d20..3a670fb76 100644 --- a/net/http/http_auth_controller.cc +++ b/net/http/http_auth_controller.cc @@ -15,6 +15,7 @@ #include "base/values.h" #include "net/base/auth.h" #include "net/base/url_util.h" +#include "net/cert/x509_certificate.h" #include "net/dns/host_resolver.h" #include "net/http/http_auth_handler.h" #include "net/http/http_auth_handler_factory.h" @@ -27,12 +28,24 @@ #include "net/log/net_log_source.h" #include "net/log/net_log_source_type.h" #include "net/log/net_log_with_source.h" +#include "net/ssl/ssl_info.h" #include "url/scheme_host_port.h" namespace net { namespace { +bool ServerCertMatches(const X509Certificate* handler_cert, + const X509Certificate* challenge_cert) { + if (handler_cert == challenge_cert) { + return true; + } + if (!handler_cert || !challenge_cert) { + return false; + } + return handler_cert->EqualsExcludingChain(challenge_cert); +} + enum AuthTarget { AUTH_TARGET_PROXY = 0, AUTH_TARGET_SECURE_PROXY, @@ -177,6 +190,7 @@ identity_.invalid = false; identity_.credentials = entry->credentials(); handler_.swap(handler_preemptive); + handler_server_cert_.reset(); return true; } @@ -208,6 +222,16 @@ net_log_.BeginEventReferencingSource(NetLogEventType::AUTH_HANDLE_CHALLENGE, caller_net_log.source()); + // A connection-based handler derives channel bindings from the server + // certificate at creation time. If the underlying connection has been + // replaced with one that presents a different certificate, that state is + // no longer valid for the current connection, so drop the handler and + // start the scheme over. + if (handler_ && handler_->is_connection_based() && + !ServerCertMatches(handler_server_cert_.get(), ssl_info.cert.get())) { + InvalidateCurrentHandler(INVALIDATE_HANDLER); + } + // Give the existing auth handler first try at the authentication headers. // This will also evict the entry in the HttpAuthCache if the previous // challenge appeared to be rejected, or is using a stale nonce in the Digest @@ -267,6 +291,7 @@ network_anonymization_key_, target_, auth_scheme_host_port_, disabled_schemes_, net_log_, host_resolver_, &handler_); if (handler_.get()) { + handler_server_cert_ = ssl_info.cert; HistogramAuthEvent(AUTH_EVENT_START); } } @@ -396,6 +421,7 @@ } handler_.reset(); + handler_server_cert_.reset(); identity_ = HttpAuth::Identity(); } diff --git a/net/http/http_auth_controller.h b/net/http/http_auth_controller.h index 886647ff..c1113ae 100644 --- a/net/http/http_auth_controller.h +++ b/net/http/http_auth_controller.h @@ -34,6 +34,7 @@ class NetLogWithSource; struct HttpRequestInfo; class SSLInfo; +class X509Certificate; // HttpAuthController is the main entry point for external callers into the HTTP // authentication stack. A single instance of an HttpAuthController can be used @@ -220,6 +221,13 @@ // associated auth handler. std::unique_ptr<HttpAuthHandler> handler_; + // The server certificate from the connection on which |handler_| was + // created. Connection-based handlers derive per-connection state (such as + // channel bindings) from this certificate at creation time, so the handler + // must be dropped if a later challenge arrives over a connection with a + // different certificate. + scoped_refptr<X509Certificate> handler_server_cert_; + // |identity_| holds the credentials that should be used by the handler_ to // generate challenge responses. This identity can come from a number of // places (url, cache, prompt). diff --git a/net/http/http_auth_controller_unittest.cc b/net/http/http_auth_controller_unittest.cc index 741d9342..720ee073 100644 --- a/net/http/http_auth_controller_unittest.cc +++ b/net/http/http_auth_controller_unittest.cc @@ -23,6 +23,8 @@ #include "net/log/test_net_log.h" #include "net/log/test_net_log_util.h" #include "net/ssl/ssl_info.h" +#include "net/test/cert_test_util.h" +#include "net/test/test_data_directory.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { @@ -300,4 +302,144 @@ &request, CompletionOnceCallback(), dummy_log)); } +// Tests that a connection-based auth handler is reused when a subsequent +// challenge is received over a connection with the same server certificate. +TEST(HttpAuthControllerTest, + ConnectionBasedHandlerReusedWhenCertificateUnchanged) { + NetLogWithSource dummy_log; + HttpRequestInfo request; + request.method = "GET"; + request.url = GURL("https://example.com"); + + scoped_refptr<HttpResponseHeaders> initial_headers( + HeadersFromString("HTTP/1.1 401\r\n" + "WWW-Authenticate: Mock\r\n" + "\r\n")); + // A later-round challenge with a scheme token, as a multi-round scheme would + // send after receiving the first client token. + scoped_refptr<HttpResponseHeaders> continuation_headers( + HeadersFromString("HTTP/1.1 401\r\n" + "WWW-Authenticate: Mock token\r\n" + "\r\n")); + + scoped_refptr<X509Certificate> cert = + ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem"); + ASSERT_TRUE(cert); + + SSLInfo ssl_info; + ssl_info.cert = cert; + + auto host_resolver = std::make_unique<MockHostResolver>(); + HttpAuthCache dummy_auth_cache( + false /* key_server_entries_by_network_anonymization_key */); + HttpAuthHandlerMock::Factory auth_handler_factory; + auth_handler_factory.set_do_init_from_challenge(true); + + auto first_handler = std::make_unique<HttpAuthHandlerMock>(); + first_handler->set_connection_based(true); + auth_handler_factory.AddMockHandler(std::move(first_handler), + HttpAuth::AUTH_SERVER); + auto second_handler = std::make_unique<HttpAuthHandlerMock>(); + second_handler->set_connection_based(true); + HttpAuthHandlerMock* second_handler_ptr = second_handler.get(); + auth_handler_factory.AddMockHandler(std::move(second_handler), + HttpAuth::AUTH_SERVER); + + scoped_refptr<HttpAuthController> controller( + base::MakeRefCounted<HttpAuthController>( + HttpAuth::AUTH_SERVER, GURL("https://example.com"), + NetworkAnonymizationKey(), &dummy_auth_cache, &auth_handler_factory, + host_resolver.get())); + + ASSERT_EQ(OK, controller->HandleAuthChallenge(initial_headers, ssl_info, + false, false, dummy_log)); + ASSERT_TRUE(controller->HaveAuthHandler()); + controller->ResetAuth(AuthCredentials(u"user", u"pass")); + ASSERT_TRUE(controller->HaveAuth()); + ASSERT_EQ(OK, controller->MaybeGenerateAuthToken(
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/net/http/http_auth_controller_unittest.cc b/net/http/http_auth_controller_unittest.cc
index 741d9342..720ee073 100644
--- a/net/http/http_auth_controller_unittest.cc
+++ b/net/http/http_auth_controller_unittest.cc
@@ -23,6 +23,8 @@
#include "net/log/test_net_log.h"
#include "net/log/test_net_log_util.h"
#include "net/ssl/ssl_info.h"
+#include "net/test/cert_test_util.h"
+#include "net/test/test_data_directory.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace net {
@@ -300,4 +302,144 @@
&request, CompletionOnceCallback(), dummy_log));
}
+// Tests that a connection-based auth handler is reused when a subsequent
+// challenge is received over a connection with the same server certificate.
+TEST(HttpAuthControllerTest,
+ ConnectionBasedHandlerReusedWhenCertificateUnchanged) {
+ NetLogWithSource dummy_log;
+ HttpRequestInfo request;
+ request.method = "GET";
+ request.url = GURL("https://example.com");
+
+ scoped_refptr<HttpResponseHeaders> initial_headers(
+ HeadersFromString("HTTP/1.1 401\r\n"
+ "WWW-Authenticate: Mock\r\n"
+ "\r\n"));
+ // A later-round challenge with a scheme token, as a multi-round scheme would
+ // send after receiving the first client token.
+ scoped_refptr<HttpResponseHeaders> continuation_headers(
+ HeadersFromString("HTTP/1.1 401\r\n"
+ "WWW-Authenticate: Mock token\r\n"
+ "\r\n"));
+
+ scoped_refptr<X509Certificate> cert =
+ ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem");
+ ASSERT_TRUE(cert);
+
+ SSLInfo ssl_info;
+ ssl_info.cert = cert;
+
+ auto host_resolver = std::make_unique<MockHostResolver>();
+ HttpAuthCache dummy_auth_cache(
+ false /* key_server_entries_by_network_anonymization_key */);
+ HttpAuthHandlerMock::Factory auth_handler_factory;
+ auth_handler_factory.set_do_init_from_challenge(true);
+
+ auto first_handler = std::make_unique<HttpAuthHandlerMock>();
+ first_handler->set_connection_based(true);
+ auth_handler_factory.AddMockHandler(std::move(first_handler),
+ HttpAuth::AUTH_SERVER);
+ auto second_handler = std::make_unique<HttpAuthHandlerMock>();
+ second_handler->set_connection_based(true);
+ HttpAuthHandlerMock* second_handler_ptr = second_handler.get();
+ auth_handler_factory.AddMockHandler(std::move(second_handler),
+ HttpAuth::AUTH_SERVER);
+
+ scoped_refptr<HttpAuthController> controller(
+ base::MakeRefCounted<HttpAuthController>(
+ HttpAuth::AUTH_SERVER, GURL("https://example.com"),
+ NetworkAnonymizationKey(), &dummy_auth_cache, &auth_handler_factory,
+ host_resolver.get()));
+
+ ASSERT_EQ(OK, controller->HandleAuthChallenge(initial_headers, ssl_info,
+ false, false, dummy_log));
+ ASSERT_TRUE(controller->HaveAuthHandler());
+ controller->ResetAuth(AuthCredentials(u"user", u"pass"));
+ ASSERT_TRUE(controller->HaveAuth());
+ ASSERT_EQ(OK, controller->MaybeGenerateAuthToken(
+ &request, CompletionOnceCallback(), dummy_log));
+
+ ASSERT_EQ(OK, controller->HandleAuthChallenge(continuation_headers, ssl_info,
+ false, false, dummy_log));
+ // The certificate did not change, so the original handler continues and
+ // the second handler is never initialized.
+ EXPECT_TRUE(controller->HaveAuthHandler());
+ EXPECT_EQ(HttpAuthHandlerMock::State::WAIT_FOR_INIT,
+ second_handler_ptr->state());
+}
+
+// Tests that a connection-based auth handler is dropped and re-created when a
+// subsequent challenge is received over a connection with a different server
+// certificate than the one the handler was created on. Connection-based
+// handlers derive channel bindings from the certificate at creation time, so
+// they must not be reused across connections with different certificates.
+TEST(HttpAuthControllerTest, ConnectionBasedHandlerDroppedOnCertificateChange) {
+ NetLogWithSource dummy_log;
+ HttpRequestInfo request;
+ request.method = "GET";
+ request.url = GURL("https://example.com");
+
+ scoped_refptr<HttpResponseHeaders> initial_headers(
+ HeadersFromString("HTTP/1.1 401\r\n"
+ "WWW-Authenticate: Mock\r\n"
+ "\r\n"));
+ // A later-round challenge with a scheme token, as a multi-round scheme would
+ // send after receiving the first client token.
+ scoped_refptr<HttpResponseHeaders> continuation_headers(
+ HeadersFromString("HTTP/1.1 401\r\n"
+ "WWW-Authenticate: Mock token\r\n"
+ "\r\n"));
+
+ scoped_refptr<X509Certificate> cert1 =
+ ImportCertFromFile(GetTestCertsDirectory(), "ok_cert.pem");
+ ASSERT_TRUE(cert1);
+ scoped_refptr<X509Certificate> cert2 =
+ ImportCertFromFile(GetTestCertsDirectory(), "wildcard.pem");
+ ASSERT_TRUE(cert2);
+ ASSERT_FALSE(cert1->EqualsExcludingChain(cert2.get()));
+
+ SSLInfo ssl_info1;
+ ssl_info1.cert = cert1;
+ SSLInfo ssl_info2;
+ ssl_info2.cert = cert2;
+
+ auto host_resolver = std::make_unique<MockHostResolver>();
+ HttpAuthCache dummy_auth_cache(
+ false /* key_server_entries_by_network_anonymization_key */);
+ HttpAuthHandlerMock::Factory auth_handler_factory;
+ auth_handler_factory.set_do_init_from_challenge(true);
+
+ auto first_handler = std::make_unique<HttpAuthHandlerMock>();
+ first_handler->set_connection_based(true);
+ auth_handler_factory.AddMockHandler(std::move(first_handler),
+ HttpAuth::AUTH_SERVER);
+ auto second_handler = std::make_unique<HttpAuthHandlerMock>();
+ second_handler->set_connection_based(true);
+ HttpAuthHandlerMock* second_handler_ptr = second_handler.get();
+ auth_handler_factory.AddMockHandler(std::move(second_handler),
+ HttpAuth::AUTH_SERVER);
+
+ scoped_refptr<HttpAuthController> controller(
+ base::MakeRefCounted<HttpAuthController>(
+ HttpAuth::AUTH_SERVER, GURL("https://example.com"),
+ NetworkAnonymizationKey(), &dummy_auth_cache, &auth_handler_factory,
+ host_resolver.get()));
+
+ ASSERT_EQ(OK, controller->HandleAuthChallenge(initial_headers, ssl_info1,
+ false, false, dummy_log));
+ ASSERT_TRUE(controller->HaveAuthHandler());
+ controller->ResetAuth(AuthCredentials(u"user", u"pass"));
+ ASSERT_TRUE(controller->HaveAuth());
+ ASSERT_EQ(OK, controller->MaybeGenerateAuthToken(
+ &request, CompletionOnceCallback(), dummy_log));
+
+ ASSERT_EQ(OK, controller->HandleAuthChallenge(continuation_headers, ssl_info2,
+ false, false, dummy_log));
+ // The certificate changed, so the original handler must have been dropped
+ // and a new one created from the current challenge with the new SSLInfo.
+ EXPECT_TRUE(controller->HaveAuthHandler());
+ EXPECT_NE(HttpAuthHandlerMock::State::WAIT_FOR_INIT,
+ second_handler_ptr->state());
+}
+
} // namespace net
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page