CVE-2025-13640
Overview
Background
- HTTP auth autofill
- when a site or proxy answers with an HTTP Basic/Digest challenge, Chrome shows a native login prompt, and the password manager can pre-fill it with a saved credential through
ChromePasswordManagerClient::AutofillHttpAuthandHttpAuthManagerImpl::Autofill, which pass the username and password to anHttpAuthObserverviaOnAutofillDataAvailable. - Biometric re-auth before filling
- a password manager setting, checked through
PasswordManagerClient::IsReauthBeforeFillingRequired, that requires the user to pass a device authentication (for example Touch ID or Windows Hello) through adevice_reauth::DeviceAuthenticatorbefore a saved password is filled. - `DeviceAuthenticator`
- the platform abstraction that shows the OS authentication prompt (
AuthenticateWithMessage) and reports a boolean result to a callback; it can be cancelled withCancel(). - `content::Visibility::HIDDEN`
- the visibility state of a
WebContentswhose tab is not currently shown to the user.
Root Cause Analysis
Before the patch, HttpAuthManagerImpl::Autofill checked only that an observer was attached, that the form manager matched, and that client_->IsFillingEnabled(...). It then called observer_->OnAutofillDataAvailable(...) with the stored username and password straight away. It never consulted IsReauthBeforeFillingRequired, so the HTTP auth prompt was filled without the biometric check that the user had asked for. HTML form filling enforces that check, so the HTTP auth path was an inconsistent bypass. ChromePasswordManagerClient::AutofillHttpAuth also filled unconditionally, even when the tab was hidden, and then called PasswordWasAutofilled right away.
The fix adds an early return when web_contents()->GetVisibility() == content::Visibility::HIDDEN. It also makes HttpAuthManagerImpl::Autofill fetch a DeviceAuthenticator and, when re-auth is required, defer filling to OnReauthCompleted. That function hands the credentials to the observer only if auth_result is true. PasswordWasAutofilled is now passed as a completion callback, with a copy of the best matches, because the form manager may not outlive the authentication.
DeviceAuthenticator prompt and refuses to fill when the tab is not visible.Attack Path
- Trigger an HTTP auth challenge
a page, or a resource it loads, gets a 401/407 challenge for a realm for which the victim has a saved credential, so the browser builds an HTTP auth form manager and calls
ChromePasswordManagerClient::AutofillHttpAuth. - Skip the re-auth gate
before the fix,
HttpAuthManagerImpl::Autofillpassed the stored username and password to the auth prompt observer without callingIsReauthBeforeFillingRequiredor showing aDeviceAuthenticatorprompt. - Fill while not visible
the old
AutofillHttpAuthdid not check tab visibility, so the fill could also happen in a background (hidden) tab that the user could not see. - Credential exposure someone at the browser, or a flow that submits the filled prompt, could get the saved credential without passing the biometric check the user had enabled (inferred).
Impact Assessment
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
Audit Directions
- Parallel fill paths skipping re-authcheck every credential-release path (HTTP auth, manual fallback, the account/keyboard accessory, touch-to-fill, the credential management API, password export/view) for the same
IsReauthBeforeFillingRequired/DeviceAuthenticatorgate that HTML form filling uses. - Visibility / focus gatinglook for autofill or credential-surfacing code that acts on background, hidden or prerendered
WebContentswithout a visibility check. - Async auth callbacks and lifetimewhere filling becomes asynchronous behind an auth prompt, make sure callbacks are bound with weak pointers, that copied state does not depend on objects that may die (such as
PasswordFormManagerForUI), and that pending authenticators are cancelled on teardown.
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