CVE-2025-13640
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/password_manager/core/browser/http_auth_manager_impl.cc |
modified | |
DeviceAuthenticatorcomponents/password_manager/core/browser/http_auth_manager_impl.h |
modified | |
PasswordManagerClientcomponents/password_manager/core/browser/http_auth_manager_impl.h |
modified |
Files Changed
chrome/browser/password_manager/chrome_password_manager_client.cccomponents/password_manager/core/browser/http_auth_manager_impl.cccomponents/password_manager/core/browser/http_auth_manager_impl.hcomponents/password_manager/core/browser/http_auth_manager_unittest.cc
Patch
From 98adca1518214906f219902deb50aa01073db37f Mon Sep 17 00:00:00 2001
From: Viktor Semeniuk <vsemeniuk@google.com>
Date: Thu, 23 Oct 2025 03:17:47 -0700
Subject: [PATCH] Prompt biometric reauth before filling password for basic auth
Fixed: 452071826
Change-Id: I3b91a14f3ef5ace86de024d54178356c14d04dfc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7046232
Commit-Queue: Viktor Semeniuk <vsemeniuk@google.com>
Reviewed-by: Vasilii Sukhanov <vasilii@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1534201}
---
diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 840dee7..a851eac 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -912,11 +912,27 @@
void ChromePasswordManagerClient::AutofillHttpAuth(
const PasswordForm& preferred_match,
const password_manager::PasswordFormManagerForUI* form_manager) {
- httpauth_manager_.Autofill(preferred_match, form_manager);
- DCHECK(!form_manager->GetBestMatches().empty());
- PasswordWasAutofilled(form_manager->GetBestMatches(),
- url::Origin::Create(form_manager->GetURL()), {},
- /*was_autofilled_on_pageload=*/false);
+ if (web_contents()->GetVisibility() == content::Visibility::HIDDEN) {
+ // Do not autofill credentials if current tab is not visible.
+ return;
+ }
+
+ CHECK(!form_manager->GetBestMatches().empty());
+
+ // Make a copy of best matches as form_manager is not guaranteed to outlive
+ // authentication.
+ std::vector<PasswordForm> best_matches;
+ for (const auto& result : form_manager->GetBestMatches()) {
+ best_matches.emplace_back(result);
+ }
+
+ httpauth_manager_.Autofill(
+ preferred_match, form_manager,
+ base::BindOnce(&ChromePasswordManagerClient::PasswordWasAutofilled,
+ weak_ptr_factory_.GetWeakPtr(), std::move(best_matches),
+ url::Origin::Create(form_manager->GetURL()),
+ base::span<const PasswordForm>(),
+ /*was_autofilled_on_pageload=*/false));
}
void ChromePasswordManagerClient::NotifyUserCredentialsWereLeaked(
diff --git a/components/password_manager/core/browser/http_auth_manager_impl.cc b/components/password_manager/core/browser/http_auth_manager_impl.cc
index 3853370..941e30b 100644
--- a/components/password_manager/core/browser/http_auth_manager_impl.cc
+++ b/components/password_manager/core/browser/http_auth_manager_impl.cc
@@ -6,7 +6,9 @@
#include <utility>
+#include "base/task/single_thread_task_runner.h"
#include "components/autofill/core/common/save_password_progress_logger.h"
+#include "components/device_reauth/device_authenticator.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_form_manager.h"
#include "components/password_manager/core/browser/password_form_manager_for_ui.h"
@@ -36,6 +38,9 @@
if (observer_) {
observer_->OnLoginModelDestroying();
}
+ if (authenticator_) {
+ authenticator_->Cancel();
+ }
}
void HttpAuthManagerImpl::DetachObserver(HttpAuthObserver* observer) {
@@ -75,14 +80,57 @@
}
}
-void HttpAuthManagerImpl::Autofill(
- const PasswordForm& preferred_match,
- const PasswordFormManagerForUI* form_manager) const {
- DCHECK_NE(PasswordForm::Scheme::kHtml, preferred_match.scheme);
- if (observer_ && (form_manager_.get() == form_manager) &&
- client_->IsFillingEnabled(form_manager_->GetURL())) {
+void HttpAuthManagerImpl::Autofill(const PasswordForm& preferred_match,
+ const PasswordFormManagerForUI* form_manager,
+ base::OnceClosure on_filling_complete) {
+ CHECK_NE(PasswordForm::Scheme::kHtml, preferred_match.scheme);
+ if (!observer_ || form_manager_.get() != form_manager) {
+ return;
+ }
+
+ if (!client_->IsFillingEnabled(form_manager_->GetURL())) {
+ return;
+ }
+
+ std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator =
+ client_->GetDeviceAuthenticator();
+
+ // Biometric filling is disabled, notify observers and invoke callback.
+ if (!client_->IsReauthBeforeFillingRequired(authenticator.get())) {
observer_->OnAutofillDataAvailable(preferred_match.username_value,
preferred_match.password_value);
+ base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
+ FROM_HERE, std::move(on_filling_complete));
+ return;
+ }
+
+ auto filling_callback = base::BindOnce(
+ &HttpAuthManagerImpl::OnReauthCompleted, weak_ptr_factory_.GetWeakPtr(),
+ preferred_match.username_value, preferred_match.password_value,
+ std::move(on_filling_complete));
+ if (authenticator_) {
+ authenticator_->Cancel();
+ }
+ authenticator_ = std::move(authenticator);
+
+ std::u16string message;
+#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+ const std::u16string origin = base::UTF8ToUTF16(
+ GetShownOrigin(url::Origin::Create(client_->GetLastCommittedURL())));
+ message =
+ l10n_util::GetStringFUTF16(IDS_PASSWORD_MANAGER_FILLING_REAUTH, origin);
+#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+ authenticator_->AuthenticateWithMessage(message, std::move(filling_callback));
+}
+
+void HttpAuthManagerImpl::OnReauthCompleted(
+ const std::u16string& username,
+ const std::u16string& password,
+ base::OnceClosure on_filling_complete,
+ bool auth_result) {
+ if (auth_result) {
+ observer_->OnAutofillDataAvailable(username, password);
+ std::move(on_filling_complete).Run();
}
}
diff --git a/components/password_manager/core/browser/http_auth_manager_impl.h b/components/password_manager/core/browser/http_auth_manager_impl.h
index 3bc849e5..c3248d1 100644
--- a/components/password_manager/core/browser/http_auth_manager_impl.h
+++ b/components/password_manager/core/browser/http_auth_manager_impl.h
@@ -13,6 +13,10 @@
#include "components/password_manager/core/browser/http_auth_manager.h"
#include "components/password_manager/core/browser/http_auth_observer.h"
+namespace device_reauth {
+class DeviceAuthenticator;
+}
+
namespace password_manager {
class PasswordManagerClient;
@@ -40,11 +44,14 @@
void OnPasswordFormDismissed() override;
// Called by a PasswordManagerClient when it decides that a HTTP auth dialog
- // can be auto-filled. It notifies the observer about new credentials given
+ // can be auto-filled. If biometric auth before filling is enabled prompt
+ // authentication first. It notifies the observer about new credentials given
// that the form manged by |form_manager| equals the one observed by the
- // observer that is managed by |form_manager|.
+ // observer that is managed by |form_manager|. |on_filling_complete| is
+ // invoked after filling is completed.
void Autofill(const PasswordForm& preferred_match,
- const PasswordFormManagerForUI* form_manager) const;
+ const PasswordFormManagerForUI* form_manager,
+ base::OnceClosure on_filling_complete);
// Handles successful navigation to the main frame.
void OnDidFinishMainFrameNavigation();
@@ -60,6 +67,11 @@
// Initiates the saving of the password.
void OnLoginSuccesfull();
+ void OnReauthCompleted(const std::u16string& username,
+ const std::u16string& password,
+ base::OnceClosure on_filling_complete,
+ bool auth_result);
+
// The embedder-level client. Must outlive this class.
const raw_ptr<PasswordManagerClient> client_;
@@ -72,6 +84,11 @@
// When set to true, the password form has been dismissed and |form_manager_|
// will be cleared on next navigation.
bool form_dismissed_ = false;
+
+ // The authenticator used to trigger a biometric re-auth before filling.
+ std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator_;
+
+ base::WeakPtrFactory<HttpAuthManagerImpl> weak_ptr_factory_{this};
};
} // namespace password_manager
diff --git a/components/password_manager/core/browser/http_auth_manager_unittest.cc b/components/password_manager/core/browser/http_auth_manager_unittest.cc
index 90687dd..04bba040 100644
--- a/components/password_manager/core/browser/http_auth_manager_unittest.cc
+++ b/components/password_manager/core/browser/http_auth_manager_unittest.cc
@@ -10,13 +10,17 @@
Regression Test / PoC
diff --git a/components/password_manager/core/browser/http_auth_manager_unittest.cc b/components/password_manager/core/browser/http_auth_manager_unittest.cc
index 90687dd..04bba040 100644
--- a/components/password_manager/core/browser/http_auth_manager_unittest.cc
+++ b/components/password_manager/core/browser/http_auth_manager_unittest.cc
@@ -10,13 +10,17 @@
#include "base/feature_list.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
+#include "base/test/gmock_callback_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
+#include "base/test/mock_callback.h"
#include "base/test/task_environment.h"
+#include "base/test/test_future.h"
#include "base/test/test_mock_time_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/autofill/core/common/form_field_data.h"
+#include "components/device_reauth/mock_device_authenticator.h"
#include "components/password_manager/core/browser/form_fetcher_impl.h"
#include "components/password_manager/core/browser/http_auth_manager_impl.h"
#include "components/password_manager/core/browser/password_form.h"
@@ -70,6 +74,14 @@
(const, override));
MOCK_METHOD(void, PromptUserToSaveOrUpdatePasswordPtr, (), ());
MOCK_METHOD(PrefService*, GetPrefs, (), (const, override));
+ MOCK_METHOD(bool,
+ IsReauthBeforeFillingRequired,
+ (device_reauth::DeviceAuthenticator*),
+ (override));
+ MOCK_METHOD(std::unique_ptr<device_reauth::DeviceAuthenticator>,
+ GetDeviceAuthenticator,
+ (),
+ (override));
// Workaround for std::unique_ptr<> lacking a copy constructor.
bool PromptUserToSaveOrUpdatePassword(
@@ -138,10 +150,6 @@
httpauth_manager_ = std::make_unique<HttpAuthManagerImpl>(&client_);
EXPECT_CALL(*store_, IsAbleToSavePasswords()).WillRepeatedly(Return(true));
-
- ON_CALL(client_, AutofillHttpAuth(_, _))
- .WillByDefault(
- Invoke(httpauth_manager_.get(), &HttpAuthManagerImpl::Autofill));
}
HttpAuthManagerImpl* httpauth_manager() { return httpauth_manager_.get(); }
@@ -155,8 +163,18 @@
std::unique_ptr<HttpAuthManagerImpl> httpauth_manager_;
};
-TEST_P(HttpAuthManagerTest, HttpAuthFilling) {
- EXPECT_CALL(client_, IsFillingEnabled(_)).WillRepeatedly(Return(true));
+TEST_P(HttpAuthManagerTest, HttpAuthFillingReauthNotRequired) {
+ EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true));
+ EXPECT_CALL(client_, IsReauthBeforeFillingRequired)
+ .WillRepeatedly(Return(false));
+ base::test::TestFuture<void> future;
+ EXPECT_CALL(client_, AutofillHttpAuth)
+ .WillRepeatedly(
+ [&](const password_manager::PasswordForm& preferred_match,
+ const password_manager::PasswordFormManagerForUI* form_manager) {
+ httpauth_manager()->Autofill(preferred_match, form_manager,
+ future.GetCallback());
+ });
PasswordForm observed_form;
observed_form.scheme = PasswordForm::Scheme::kBasic;
@@ -170,17 +188,119 @@
MockHttpAuthObserver observer;
base::WeakPtr<PasswordStoreConsumer> consumer;
- EXPECT_CALL(*store_, GetLogins(_, _)).WillOnce(SaveArg<1>(&consumer));
+ EXPECT_CALL(*store_, GetLogins).WillOnce(SaveArg<1>(&consumer));
+ httpauth_manager()->SetObserverAndDeliverCredentials(&observer,
+ observed_form);
+ EXPECT_CALL(observer, OnAutofillDataAvailable(std::u16string_view(u"user"),
+ std::u16string_view(u"1234")))
+ .Times(2);
+ ASSERT_TRUE(consumer);
+ std::vector<PasswordForm> result;
+ result.push_back(stored_form);
+ consumer->OnGetPasswordStoreResultsOrErrorFrom(store_.get(),
+ std::move(result));
+ ASSERT_TRUE(future.Wait());
+
+ testing::Mock::VerifyAndClearExpectations(&store_);
+ httpauth_manager()->DetachObserver(&observer);
+}
+
+// Test autofill when biometric re-auth is required and successful.
+TEST_P(HttpAuthManagerTest, HttpAuthFillingReauthSuccess) {
+ EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true));
+ EXPECT_CALL(client_, IsReauthBeforeFillingRequired)
+ .WillRepeatedly(Return(true));
+ base::MockOnceClosure mock_callback;
+ EXPECT_CALL(client_, AutofillHttpAuth)
+ .WillOnce(
+ [&](const password_manager::PasswordForm& preferred_match,
+ const password_manager::PasswordFormManagerForUI* form_manager) {
+ httpauth_manager()->Autofill(preferred_match, form_manager,
+ mock_callback.Get());
+ });
+
+ auto mock_authenticator =
+ std::make_unique<device_reauth::MockDeviceAuthenticator>();
+ EXPECT_CALL(*mock_authenticator, AuthenticateWithMessage)
+ .WillOnce(base::test::RunOnceCallback<1>(true));
+ EXPECT_CALL(client_, GetDeviceAuthenticator)
+ .WillOnce(Return(testing::ByMove(std::move(mock_authenticator))));
+
+ PasswordForm observed_form;
+ observed_form.scheme = PasswordForm::Scheme::kBasic;
+ observed_form.url = GURL("http://proxy.com/");
+ observed_form.signon_realm = "proxy.com/realm";
+
+ PasswordForm stored_form = observed_form;
+ stored_form.username_value = u"user";
+ stored_form.password_value = u"1234";
+
+ MockHttpAuthObserver observer;
+
+ base::WeakPtr<PasswordStoreConsumer> consumer;
+ EXPECT_CALL(*store_, GetLogins).WillOnce(SaveArg<1>(&consumer));
httpauth_manager()->SetObserverAndDeliverCredentials(&observer,
observed_form);
EXPECT_CALL(observer, OnAutofillDataAvailable(std::u16string_view(u"user"),
std::u16string_view(u"1234")));
+ EXPECT_CALL(mock_callback, Run);
ASSERT_TRUE(consumer);
std::vector<PasswordForm> result;
result.push_back(stored_form);
consumer->OnGetPasswordStoreResultsOrErrorFrom(store_.get(),
std::move(result));
testing::Mock::VerifyAndClearExpectations(&store_);
+
+ httpauth_manager()->DetachObserver(&observer);
+}
+
+// Test autofill when biometric re-auth is required and successful.
+TEST_P(HttpAuthManagerTest, HttpAuthFillingReauthFailure) {
+ EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true));
+ EXPECT_CALL(client_, IsReauthBeforeFillingRequired)
+ .WillRepeatedly(Return(true));
+ base::MockOnceClosure mock_callback;
+ EXPECT_CALL(client_, AutofillHttpAuth)
+ .WillOnce(
+ [&](const password_manager::PasswordForm& preferred_match,
+ const password_manager::PasswordFormManagerForUI* form_manager) {
+ httpauth_manager()->Autofill(preferred_match, form_manager,
+ mock_callback.Get());
+ });
+
+ auto mock_authenticator =
+ std::make_unique<device_reauth::MockDeviceAuthenticator>();
+ EXPECT_CALL(*mock_authenticator, AuthenticateWithMessage)
+ .WillOnce(base::test::RunOnceCallback<1>(false));
+
+ EXPECT_CALL(client_, GetDeviceAuthenticator)
+ .WillOnce(Return(testing::ByMove(std::move(mock_authenticator))));
+
+ PasswordForm observed_form;
+ observed_form.scheme = PasswordForm::Scheme::kBasic;
+ observed_form.url = GURL("http://proxy.com/");
+ observed_form.signon_realm = "proxy.com/realm";
+
+ PasswordForm stored_form = observed_form;
+ stored_form.username_value = u"user";
+ stored_form.password_value = u"1234";
+
+ MockHttpAuthObserver observer;
+
+ base::WeakPtr<PasswordStoreConsumer> consumer;
+ EXPECT_CALL(*store_, GetLogins).WillOnce(SaveArg<1>(&consumer));
+ httpauth_manager()->SetObserverAndDeliverCredentials(&observer,
+ observed_form);
+ EXPECT_CALL(observer, OnAutofillDataAvailable).Times(0);
+ EXPECT_CALL(mock_callback, Run).Times(0);
+
+ ASSERT_TRUE(consumer);
+ std::vector<PasswordForm> result;
+ result.push_back(stored_form);
+ consumer->OnGetPasswordStoreResultsOrErrorFrom(store_.get(),
+ std::move(result));
+ testing::Mock::VerifyAndClearExpectations(&store_);
+
httpauth_manager()->DetachObserver(&observer);
}
Original Bug Report
HTTP-Auth Passwords are not secured on MacOS
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS “Use your screen lock when filling passwords” on MacOS does not work for HTTP Authentication.
VERSION Chrome Version: 141.0.7390.77 stable Operating System: MacOS Tahoe 26.0 (25A354)
REPRODUCTION CASE
- Install Chrome freshly
- Open Chrome
- Open the URL chrome://password-manager/settings
- Enable the Setting “Use your screen lock when filling passwords”
- Open the URL https://authenticationtest.com/HTTPAuth/
- Enter “user” and “pass” as credentials
- Save the credentials as supposed by Chrome
- Clear website data to log out, close and reopen Chrome
- Open again the URL https://authenticationtest.com/HTTPAuth/
At this point, the passwords are already filled in and we can log in without the need to authenticate via screen lock (e.g. fingerprint)
Conversely, the screen lock to access passwords works on other authentication methods such as the one on this site: https://authenticationtest.com/simpleFormAuth/
CREDIT INFORMATION Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited? Reporter credit: Anonymous