Chrome · Network › Net
CVE-2026-113119
UAF in Network (Net)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifnet/http/http_auth_controller.cc |
modified | |
ifnet/http/http_network_transaction.cc |
modified |
Files Changed
net/http/http_proxy_client_socket.ccnet/http/http_auth_controller.ccnet/spdy/spdy_proxy_client_socket.ccnet/http/http_network_transaction.ccnet/http/http_network_transaction.hnet/http/http_auth_controller_unittest.ccnet/quic/quic_proxy_client_socket.ccnet/http/http_auth_controller.hnet/http/http_network_transaction_unittest.cc
Patch
From 6bc3005112c94fda252f66b1ebd7b520195b3de3 Mon Sep 17 00:00:00 2001 From: Sebastien Lalancette <[email protected]> Date: Fri, 25 Sep 2026 12:26:09 -0700 Subject: [PATCH] [net] Validate server certificate before generating auth token Connection-based HTTP authentication schemes (such as NTLM and Negotiate) derive per-connection state and channel bindings from the server certificate presented when the handler is created. Previously, HttpAuthController only validated certificate consistency upon receiving a challenge in HandleAuthChallenge(). If a connection was replaced after receiving a challenge or between handshake rounds, MaybeGenerateAuthToken() could generate and transmit an authentication token bound to the previous connection's certificate over a new connection presenting a different certificate. This change passes the target connection's server certificate to HttpAuthController::MaybeGenerateAuthToken() and invalidates the active handler if the certificate does not match before generating an auth token. All transaction and proxy socket callers are updated to pass the target certificate. Bug: 557075191 Change-Id: Ie219efcb29fa840ffeb9b181563b12663458d962 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8374421 Reviewed-by: mmenke <[email protected]> Commit-Queue: Sebastien Lalancette <[email protected]> Cr-Commit-Position: refs/heads/main@{#1705049} --- diff --git a/net/http/http_auth_controller.cc b/net/http/http_auth_controller.cc index 3a670fb76..cc5afa574 100644 --- a/net/http/http_auth_controller.cc +++ b/net/http/http_auth_controller.cc @@ -125,18 +125,31 @@ int HttpAuthController::MaybeGenerateAuthToken( const HttpRequestInfo* request, + const SSLInfo& ssl_info, CompletionOnceCallback callback, const NetLogWithSource& caller_net_log) { DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); DCHECK(!auth_info_); + + // A connection-based handler derives per-connection state (such as channel + // bindings) from the server certificate at creation time. If the + // underlying connection has been replaced with one that presents a + // different certificate since the last challenge, that state is no longer + // valid for the current connection, so drop the handler instead of + // generating a token from it. The next challenge starts the authentication + // protocol over on the current connection. + InvalidateIfServerCertChanged(ssl_info.cert.get()); + bool needs_auth = HaveAuth() || SelectPreemptiveAuth(caller_net_log); - if (!needs_auth) + if (!needs_auth) { return OK; + } net_log_.BeginEventReferencingSource(NetLogEventType::AUTH_GENERATE_TOKEN, caller_net_log.source()); const AuthCredentials* credentials = nullptr; - if (identity_.source != HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS) + if (identity_.source != HttpAuth::IDENT_SRC_DEFAULT_CREDENTIALS) { credentials = &identity_.credentials; + } DCHECK(auth_token_.empty()); DCHECK(callback_.is_null()); int rv = handler_->GenerateAuthToken( @@ -161,8 +174,9 @@ // Don't do preemptive authorization if the URL contains a username:password, // since we must first be challenged in order to use the URL's identity. - if (auth_url_.has_username()) + if (auth_url_.has_username()) { return false; + } // SelectPreemptiveAuth() is on the critical path for each request, so it // is expected to be fast. LookupByPath() is fast in the common case, since @@ -170,8 +184,9 @@ // (For most users in fact, it will be 0.) HttpAuthCache::Entry* entry = http_auth_cache_->LookupByPath( auth_scheme_host_port_, target_, network_anonymization_key_, auth_path_); - if (!entry) + if (!entry) { return false; + } BindToCallingNetLog(caller_net_log); @@ -226,11 +241,8 @@ // 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); - } + // start the authentication protocol over. + InvalidateIfServerCertChanged(ssl_info.cert.get()); // Give the existing auth handler first try at the authentication headers. // This will also evict the entry in the HttpAuthCache if the previous @@ -673,4 +685,13 @@ InvalidateCurrentHandler(INVALIDATE_HANDLER); } +void HttpAuthController::InvalidateIfServerCertChanged( + const X509Certificate* cert) { + DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); + if (handler_ && handler_->is_connection_based() && + !ServerCertMatches(handler_server_cert_.get(), cert)) { + InvalidateCurrentHandler(INVALIDATE_HANDLER); + } +} + } // namespace net diff --git a/net/http/http_auth_controller.h b/net/http/http_auth_controller.h index 7d5cbf5e..136c79c8a 100644 --- a/net/http/http_auth_controller.h +++ b/net/http/http_auth_controller.h @@ -93,11 +93,20 @@ HttpAuthHandlerFactory* http_auth_handler_factory, HostResolver* host_resolver); - // Generate an authentication token for |target| if necessary. The return - // value is a net error code. |OK| will be returned both in the case that + // Generate an authentication token for `target` if necessary. The return + // value is a net error code. `OK` will be returned both in the case that // a token is correctly generated synchronously, as well as when no tokens // were necessary. + // + // `ssl_info` is the certificate information for the connection the token + // will be sent over, and must match what HandleAuthChallenge() is given for + // responses on that connection. It is empty when no certificate applies to + // this controller's target, for example for "http" origins and for proxy + // auth. If a connection-based handler was created on a connection with a + // different server certificate, it is dropped instead of generating a token, + // so that authentication restarts on the current connection. int MaybeGenerateAuthToken(const HttpRequestInfo* request, + const SSLInfo& ssl_info, CompletionOnceCallback callback, const NetLogWithSource& net_log); @@ -207,6 +216,10 @@ // Records the number of authentication events per authentication scheme. void HistogramAuthEvent(AuthEvent auth_event); + // If the current handler is connection-based and was created for a connection + // with a different server certificate, invalidates the handler. + void InvalidateIfServerCertChanged(const X509Certificate* cert); + // Indicates if this handler is for Proxy auth or Server auth. HttpAuth::Target target_; diff --git a/net/http/http_network_transaction.cc b/net/http/http_network_transaction.cc index 1788baf8..6b28ed9 100644 --- a/net/http/http_network_transaction.cc +++ b/net/http/http_network_transaction.cc @@ -1007,6 +1007,15 @@ request_->url.SchemeIs("http"); } +SSLInfo HttpNetworkTransaction::GetStreamSSLInfo() { + CHECK(stream_); + SSLInfo ssl_info; + if (IsSecureRequest()) { + stream_->GetSSLInfo(&ssl_info); + } + return ssl_info; +} + void HttpNetworkTransaction::DoCallback(int rv) { DCHECK_NE(rv, ERR_IO_PENDING); DCHECK(!callback_.is_null()); @@ -1335,8 +1344,9 @@ int HttpNetworkTransaction::DoGenerateProxyAuthToken() { next_state_ = STATE_GENERATE_PROXY_AUTH_TOKEN_COMPLETE; - if (!ShouldApplyProxyAuth()) + if (!ShouldApplyProxyAuth()) { return OK; + } HttpAuth::Target target = HttpAuth::AUTH_PROXY; GURL auth_url = AuthURL(target); // The proxy may have changed (for example due to fallback to another proxy @@ -1350,13 +1360,25 @@ url::SchemeHostPort(auth_url))) { auth_controllers_[target] = nullptr; } - if (!auth_controllers_[target].get()) + if (!auth_controllers_[target].get()) { auth_controllers_[target] = base::MakeRefCounted<HttpAuthController>( target, auth_url, request_->network_anonymization_key, session_->http_auth_cache(), session_->http_auth_handler_factory(), session_->host_resolver()); + } + // This is only reached for GET proxies (see ShouldApplyProxyAuth()); CONNECT + // proxy auth is handled by the proxy client sockets instead. GET proxy auth + // controllers are never given a certificate, so pass an empty SSLInfo. + // + // GetStreamSSLInfo() would also return an empty SSLInfo here, because a GET + // proxy is only used for "http" requests, so IsSecureRequest() is false. It + // is not used, to avoid implying that the proxy's certificate could show up + // here: GetStreamSSLInfo() only describes the destination server. + // + // As a result, GET proxy auth handlers derive no channel bindings and are not + // pinned to the proxy's certificate. This is legacy behavior. return auth_controllers_[target]->MaybeGenerateAuthToken( - request_, io_callback_, net_log_); + request_, SSLInfo(), io_callback_, net_log_); } int HttpNetworkTransaction::DoGenerateProxyAuthTokenComplete(int rv) { @@ -1374,13 +1396,15 @@ target, AuthURL(target), request_->network_anonymization_key, session_->http_auth_cache(), session_->http_auth_handler_factory(), session_->host_resolver()); - if (request_->load_flags & LOAD_DO_NOT_USE_EMBEDDED_IDENTITY) + if (request_->load_flags & LOAD_DO_NOT_USE_EMBEDDED_IDENTITY) { auth_controllers_[target]->DisableEmbeddedIdentity(); + } } - if (!ShouldApplyServerAuth()) + if (!ShouldApplyServerAuth()) { return OK; + } return auth_controllers_[target]->MaybeGenerateAuthToken( - request_, io_callback_, net_log_); + request_, GetStreamSSLInfo(), io_callback_, net_log_); } int HttpNetworkTransaction::DoGenerateServerAuthTokenComplete(int rv) { @@ -2412,8 +2436,18 @@ if (!auth_controllers_[target].get()) return ERR_UNEXPECTED_PROXY_AUTH; + // CONNECT proxy auth challenges do not go through this path; they are + // handled by the proxy client sockets. Only challenges from GET proxies, or + // directly from servers, reach here. A proxy auth challenge sent by a server + // when there is no GET proxy results in ERR_UNEXPECTED_PROXY_AUTH above. + // + // GetStreamSSLInfo() describes the connection to the destination server, so + // only pass it to the server auth controller. The GET proxy auth controller + // gets an empty SSLInfo, matching DoGenerateProxyAuthToken(). + const SSLInfo ssl_info = + target == HttpAuth::AUTH_SERVER ? GetStreamSSLInfo() : SSLInfo(); int rv = auth_controllers_[target]->HandleAuthChallenge( - headers, response_.ssl_info, !ShouldApplyServerAuth(), false, net_log_); + headers, ssl_info, !ShouldApplyServerAuth(), false, net_log_); if (auth_controllers_[target]->HaveAuthHandler()) pending_auth_target_ = target; diff --git a/net/http/http_network_transaction.h b/net/http/http_network_transaction.h index 831fcf1..3c5a5de8 100644 --- a/net/http/http_network_transaction.h +++ b/net/http/http_network_transaction.h @@ -187,6 +187,24 @@ // tunneled via the CONNECT method. bool UsingHttpProxyWithoutTunnel() const; + // Returns the SSLInfo of the destination server's connection. This is the + // sole source of certificate information for *server* authentication, used + // by both HandleAuthChallenge() and MaybeGenerateAuthToken(), so the two are + // guaranteed to observe the same value for a given `stream_`. Proxy + // authentication never uses this; it is always passed an empty SSLInfo. + // + // Returns an empty SSLInfo for non-secure requests, even if the connection + // uses TLS: an "http" request through an HTTPS proxy runs over TLS to that + // proxy, but that certificate belongs to the proxy rather than to the + // destination server, so server auth must not observe it. + // + // Consequently this never returns a proxy's certificate. When + // `IsSecureRequest()` is true the connection is either direct or tunneled + // through CONNECT -- a GET-to-proxy connection is impossible, because + // `UsingHttpProxyWithoutTunnel()` requires an "http" scheme -- so in both + // cases the top of the socket stack terminates at the destination server. + SSLInfo GetStreamSSLInfo(); + void DoCallback(int result); void OnIOComplete(int result); diff --git a/net/http/http_proxy_client_socket.cc b/net/http/http_proxy_client_socket.cc index 2b864b0..9f72c8af 100644 --- a/net/http/http_proxy_client_socket.cc +++ b/net/http/http_proxy_client_socket.cc @@ -339,7 +339,7 @@ int HttpProxyClientSocket::DoGenerateAuthToken() { next_state_ = STATE_GENERATE_AUTH_TOKEN_COMPLETE; return auth_->MaybeGenerateAuthToken( - &request_, + &request_, response_.ssl_info, base::BindOnce(&HttpProxyClientSocket::OnIOComplete, weak_factory_.GetWeakPtr()), net_log_); diff --git a/net/quic/quic_proxy_client_socket.cc b/net/quic/quic_proxy_client_socket.cc index 7ebbcf64..5a95b2f6 100644 --- a/net/quic/quic_proxy_client_socket.cc +++ b/net/quic/quic_proxy_client_socket.cc @@ -338,7 +338,7 @@ int QuicProxyClientSocket::DoGenerateAuthToken() { next_state_ = STATE_GENERATE_AUTH_TOKEN_COMPLETE; return auth_->MaybeGenerateAuthToken( - &request_, + &request_, response_.ssl_info, base::BindOnce(&QuicProxyClientSocket::OnIOComplete, weak_factory_.GetWeakPtr()), net_log_); diff --git a/net/spdy/spdy_proxy_client_socket.cc b/net/spdy/spdy_proxy_client_socket.cc index 3963896..ba4110d 100644 --- a/net/spdy/spdy_proxy_client_socket.cc +++ b/net/spdy/spdy_proxy_client_socket.cc @@ -397,7 +397,7 @@ int SpdyProxyClientSocket::DoGenerateAuthToken() { next_state_ = STATE_GENERATE_AUTH_TOKEN_COMPLETE; return auth_->MaybeGenerateAuthToken( - &request_, + &request_, response_.ssl_info, base::BindOnce(&SpdyProxyClientSocket::OnIOComplete, weak_factory_.GetWeakPtr()), net_log_); diff --git a/net/http/http_auth_controller_unittest.cc b/net/http/http_auth_controller_unittest.cc index 856ddd21..9e576967 100644 --- a/net/http/http_auth_controller_unittest.cc +++ b/net/http/http_auth_controller_unittest.cc @@ -96,8 +96,8 @@ TestCompletionCallback callback; EXPECT_EQ( (run_mode == RUN_HANDLER_ASYNC) ? ERR_IO_PENDING : expected_controller_rv, - controller->MaybeGenerateAuthToken(&request, callback.callback(), - net_log)); + controller->MaybeGenerateAuthToken(&request, null_ssl_info, + callback.callback(), net_log)); if (run_mode == RUN_HANDLER_ASYNC) EXPECT_EQ(expected_controller_rv, callback.WaitForResult()); EXPECT_EQ((scheme_state == SCHEME_IS_DISABLED), @@ -285,8 +285,9 @@ EXPECT_TRUE(controller->HaveAuth()); // Should only succeed if we are using the AUTH_SCHEME_MOCK MockHandler. - EXPECT_EQ(OK, controller->MaybeGenerateAuthToken( - &request, CompletionOnceCallback(), dummy_log)); + EXPECT_EQ(OK, + controller->MaybeGenerateAuthToken( + &request, null_ssl_info, CompletionOnceCallback(), dummy_log)); controller->AddAuthorizationHeader(&request_headers); // Once a token is generated, simulate the receipt of a server response @@ -300,8 +301,9 @@ EXPECT_FALSE(controller->IsAuthSchemeDisabled(HttpAuth::AUTH_SCHEME_BASIC)); // Should only succeed if we are using the AUTH_SCHEME_BASIC MockHandler. - EXPECT_EQ(OK, controller->MaybeGenerateAuthToken( - &request, CompletionOnceCallback(), dummy_log)); + EXPECT_EQ(OK, + controller->MaybeGenerateAuthToken( + &request, null_ssl_info, CompletionOnceCallback(), dummy_log)); } // Tests that a connection-based auth handler is reused when a subsequent @@ -359,7 +361,7 @@ controller->ResetAuth(AuthCredentials(u"user", u"pass")); ASSERT_TRUE(controller->HaveAuth()); ASSERT_EQ(OK, controller->MaybeGenerateAuthToken( - &request, CompletionOnceCallback(), dummy_log)); + &request, ssl_info, CompletionOnceCallback(), dummy_log)); ASSERT_EQ(OK, controller->HandleAuthChallenge(continuation_headers, ssl_info, false, false, dummy_log)); @@ -433,7 +435,7 @@ controller->ResetAuth(AuthCredentials(u"user", u"pass")); ASSERT_TRUE(controller->HaveAuth()); ASSERT_EQ(OK, controller->MaybeGenerateAuthToken( - &request, CompletionOnceCallback(), dummy_log)); + &request, ssl_info1, CompletionOnceCallback(), dummy_log)); ASSERT_EQ(OK, controller->HandleAuthChallenge(continuation_headers, ssl_info2, false, false, dummy_log)); @@ -467,4 +469,124 @@ url::SchemeHostPort(GURL("https://proxy.example.com:8080")))); } +// Tests that a connection-based auth handler is dropped, without a token +// being generated from it, when the connection is replaced with one that +// presents a different server certificate before the next request is sent. +// The scheme cannot be continued preemptively, so authentication must +// restart with the next challenge. +TEST(HttpAuthControllerTest, + ConnectionBasedHandlerDroppedOnCertificateChangeBeforeGenerate) { + 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")); + + scoped_refptr<X509Certificate> cert1 = ... (file diff truncated)
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 856ddd21..9e576967 100644
--- a/net/http/http_auth_controller_unittest.cc
+++ b/net/http/http_auth_controller_unittest.cc
@@ -96,8 +96,8 @@
TestCompletionCallback callback;
EXPECT_EQ(
(run_mode == RUN_HANDLER_ASYNC) ? ERR_IO_PENDING : expected_controller_rv,
- controller->MaybeGenerateAuthToken(&request, callback.callback(),
- net_log));
+ controller->MaybeGenerateAuthToken(&request, null_ssl_info,
+ callback.callback(), net_log));
if (run_mode == RUN_HANDLER_ASYNC)
EXPECT_EQ(expected_controller_rv, callback.WaitForResult());
EXPECT_EQ((scheme_state == SCHEME_IS_DISABLED),
@@ -285,8 +285,9 @@
EXPECT_TRUE(controller->HaveAuth());
// Should only succeed if we are using the AUTH_SCHEME_MOCK MockHandler.
- EXPECT_EQ(OK, controller->MaybeGenerateAuthToken(
- &request, CompletionOnceCallback(), dummy_log));
+ EXPECT_EQ(OK,
+ controller->MaybeGenerateAuthToken(
+ &request, null_ssl_info, CompletionOnceCallback(), dummy_log));
controller->AddAuthorizationHeader(&request_headers);
// Once a token is generated, simulate the receipt of a server response
@@ -300,8 +301,9 @@
EXPECT_FALSE(controller->IsAuthSchemeDisabled(HttpAuth::AUTH_SCHEME_BASIC));
// Should only succeed if we are using the AUTH_SCHEME_BASIC MockHandler.
- EXPECT_EQ(OK, controller->MaybeGenerateAuthToken(
- &request, CompletionOnceCallback(), dummy_log));
+ EXPECT_EQ(OK,
+ controller->MaybeGenerateAuthToken(
+ &request, null_ssl_info, CompletionOnceCallback(), dummy_log));
}
// Tests that a connection-based auth handler is reused when a subsequent
@@ -359,7 +361,7 @@
controller->ResetAuth(AuthCredentials(u"user", u"pass"));
ASSERT_TRUE(controller->HaveAuth());
ASSERT_EQ(OK, controller->MaybeGenerateAuthToken(
- &request, CompletionOnceCallback(), dummy_log));
+ &request, ssl_info, CompletionOnceCallback(), dummy_log));
ASSERT_EQ(OK, controller->HandleAuthChallenge(continuation_headers, ssl_info,
false, false, dummy_log));
@@ -433,7 +435,7 @@
controller->ResetAuth(AuthCredentials(u"user", u"pass"));
ASSERT_TRUE(controller->HaveAuth());
ASSERT_EQ(OK, controller->MaybeGenerateAuthToken(
- &request, CompletionOnceCallback(), dummy_log));
+ &request, ssl_info1, CompletionOnceCallback(), dummy_log));
ASSERT_EQ(OK, controller->HandleAuthChallenge(continuation_headers, ssl_info2,
false, false, dummy_log));
@@ -467,4 +469,124 @@
url::SchemeHostPort(GURL("https://proxy.example.com:8080"))));
}
+// Tests that a connection-based auth handler is dropped, without a token
+// being generated from it, when the connection is replaced with one that
+// presents a different server certificate before the next request is sent.
+// The scheme cannot be continued preemptively, so authentication must
+// restart with the next challenge.
+TEST(HttpAuthControllerTest,
+ ConnectionBasedHandlerDroppedOnCertificateChangeBeforeGenerate) {
+ 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"));
+
+ 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 handler = std::make_unique<HttpAuthHandlerMock>();
+ handler->set_connection_based(true);
+ auth_handler_factory.AddMockHandler(std::move(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());
+
+ // The connection was replaced with one that presents a different
+ // certificate, so no token may be generated from the handler created on
+ // the original connection.
+ ASSERT_EQ(OK, controller->MaybeGenerateAuthToken(
+ &request, ssl_info2, CompletionOnceCallback(), dummy_log));
+ EXPECT_FALSE(controller->HaveAuthHandler());
+ EXPECT_FALSE(controller->HaveAuth());
+}
+
+// Tests that a handler that is not connection-based keeps generating tokens
+// when the connection is replaced with one that presents a different server
+// certificate. Such handlers derive no state from the connection.
+TEST(HttpAuthControllerTest,
+ NonConnectionBasedHandlerKeptOnCertificateChangeBeforeGenerate) {
+ NetLogWithSource dummy_log;
+ HttpRequestInfo request;
+ request.method = "GET";
+ request.url = GURL("https://example.com");
+
+ HttpRequestHeaders request_headers;
+ scoped_refptr<HttpResponseHeaders> initial_headers(
+ HeadersFromString("HTTP/1.1 401\r\n"
+ "WWW-Authenticate: Mock\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);
+
+ auth_handler_factory.AddMockHandler(std::make_unique<HttpAuthHandlerMock>(),
+ 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, ssl_info2, CompletionOnceCallback(), dummy_log));
+ EXPECT_TRUE(controller->HaveAuthHandler());
+ EXPECT_TRUE(controller->HaveAuth());
+ controller->AddAuthorizationHeader(&request_headers);
+ EXPECT_TRUE(request_headers.HasHeader(HttpRequestHeaders::kAuthorization));
+}
+
} // namespace net
diff --git a/net/http/http_network_transaction_unittest.cc b/net/http/http_network_transaction_unittest.cc
index 1493565..b606a96 100644
--- a/net/http/http_network_transaction_unittest.cc
+++ b/net/http/http_network_transaction_unittest.cc
@@ -920,13 +920,16 @@
return true;
}
+// `expected_challenger` defaults to the HTTP proxy that most tests use. Tests
+// that authenticate to an HTTPS proxy pass their own value.
bool CheckNTLMProxyAuth(
- const std::optional<AuthChallengeInfo>& auth_challenge) {
+ const std::optional<AuthChallengeInfo>& auth_challenge,
+ std::string_view expected_challenger = "http://server") {
if (!auth_challenge) {
return false;
}
EXPECT_TRUE(auth_challenge->is_proxy);
- EXPECT_EQ("http://server", auth_challenge->challenger.Serialize());
+ EXPECT_EQ(expected_challenger, auth_challenge->challenger.Serialize());
EXPECT_EQ(std::string(), auth_challenge->realm);
EXPECT_EQ(kNtlmAuthScheme, auth_challenge->scheme);
return true;
@@ -12475,6 +12478,575 @@
EXPECT_TRUE(data3.AllWriteDataConsumed());
}
+// The server closes the connection on the challenge response, so the
+// authenticate message is sent on a replacement connection. As long as the
+// replacement connection presents the same server certificate, the handshake
+// continues where it left off.
+TEST_P(HttpNetworkTransactionTest, NTLMAuthV2ContinuesAcrossReconnect) {
+ HttpRequestInfo request;
+ request.method = "GET";
+ request.url = GURL("https://server/kids/login.aspx");
+ request.traffic_annotation =
+ MutableNetworkTrafficAnnotationTag(TRAFFIC_ANNOTATION_FOR_TESTS);
+ request.load_flags = LOAD_DO_NOT_USE_EMBEDDED_IDENTITY;
+
+ HttpAuthNtlmMechanism::ScopedProcSetter proc_setter(
+ MockGetMSTime, MockGenerateRandom, MockGetHostName);
+ std::unique_ptr<HttpNetworkSession> session(CreateSession(&session_deps_));
+
+ // Generate the NTLM messages based on known test data.
+ std::string negotiate_msg = base::Base64Encode(std::string_view(
+ reinterpret_cast<const char*>(ntlm::test::kExpectedNegotiateMsg),
+ std::size(ntlm::test::kExpectedNegotiateMsg)));
+ std::string challenge_msg = base::Base64Encode(std::string_view(
+ reinterpret_cast<const char*>(ntlm::test::kChallengeMsgFromSpecV2),
+ std::size(ntlm::test::kChallengeMsgFromSpecV2)));
+ std::string authenticate_msg = base::Base64Encode(std::string_view(
+ reinterpret_cast<const char*>(
+ ntlm::test::kExpectedAuthenticateMsgEmptyChannelBindingsV2),
+ std::size(ntlm::test::kExpectedAuthenticateMsgEmptyChannelBindingsV2)));
+
+ std::string request_write2 =
+ base::StrCat({"GET /kids/login.aspx HTTP/1.1\r\n"
+ "Host: server\r\n"
+ "Connection: keep-alive\r\n"
+ "Authorization: NTLM ",
+ negotiate_msg, "\r\n\r\n"});
+
+ std::string response_read2 =
+ base::StrCat({"HTTP/1.1 401 Access Denied\r\n"
+ "WWW-Authenticate: NTLM ",
+ challenge_msg,
+ "\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 42\r\n"
+ "Content-Type: text/html\r\n\r\n"});
+
+ std::string request_write3 =
+ base::StrCat({"GET /kids/login.aspx HTTP/1.1\r\n"
+ "Host: server\r\n"
+ "Connection: keep-alive\r\n"
+ "Authorization: NTLM ",
+ authenticate_msg, "\r\n\r\n"});
+
+ MockWrite data_writes1[] = {
+ MockWrite(ASYNC, 0,
+ "GET /kids/login.aspx HTTP/1.1\r\n"
+ "Host: server\r\n"
+ "Connection: keep-alive\r\n\r\n"),
+ };
+
+ MockRead data_reads1[] = {
+ MockRead(ASYNC, 1,
+ "HTTP/1.1 401 Access Denied\r\n"
+ "WWW-Authenticate: NTLM\r\n"
+ "Connection: close\r\n"
+ "Content-Length: 42\r\n"
+ "Content-Type: text/html\r\n\r\n"),
+ // Missing content -- won't matter, as connection will be reset.
+ };
+
+ MockWrite data_writes2[] = {
+ // After restarting with a null identity, this is the
+ // request we should be issuing -- the final header line contains a Type
+ // 1 message.
+ MockWrite(ASYNC, 0, request_write2),
+ };
+
+ MockRead data_reads2[] = {
+ // The origin server responds with a Type 2 message, but closes the
+ // connection, so the Type 3 message has to be sent on a fresh one.
+ MockRead(ASYNC, 1, response_read2),
+ // Missing content -- won't matter, as connection will be reset.
+ };
+
+ MockWrite data_writes3[] = {
+ // The Type 3 message is sent on the replacement connection.
+ MockWrite(ASYNC, 0, request_write3),
+ };
+
+ MockRead data_reads3[] = {
+ // The desired content.
+ MockRead(ASYNC, 1,
+ "HTTP/1.1 200 OK\r\n"
... (truncated)
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