CVE-2026-17840
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Pcomponents/password_manager/core/browser/http_auth_manager_unittest.cc |
modified | |
forcomponents/password_manager/core/browser/http_auth_manager_unittest.cc |
modified |
Files Changed
components/password_manager/core/browser/http_auth_manager_impl.cccomponents/password_manager/core/browser/http_auth_manager_unittest.cc
Patch
From dbadf35b644fbcb6ed308419043bf0392dbe1074 Mon Sep 17 00:00:00 2001
From: Viktor Semeniuk <vsemeniuk@google.com>
Date: Mon, 22 Jun 2026 06:33:02 -0700
Subject: [PATCH] Use challenger origin in HTTP-auth biometric reauth prompt
Fixed: 518082162
Change-Id: Ibf7bd01f8c82d0941f05ca4382fc864d01630362
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7978699
Commit-Queue: Viktor Semeniuk <vsemeniuk@google.com>
Reviewed-by: Ioana Treib <ioanap@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1650273}
---
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 e242256b..1bf124db 100644
--- a/components/password_manager/core/browser/http_auth_manager_impl.cc
+++ b/components/password_manager/core/browser/http_auth_manager_impl.cc
@@ -120,7 +120,7 @@
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())));
+ GetShownOrigin(url::Origin::Create(form_manager_->GetURL())));
message =
l10n_util::GetStringFUTF16(IDS_PASSWORD_MANAGER_FILLING_REAUTH, origin);
#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
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 a16f6b9..a4a6ee9 100644
--- a/components/password_manager/core/browser/http_auth_manager_unittest.cc
+++ b/components/password_manager/core/browser/http_auth_manager_unittest.cc
@@ -62,6 +62,7 @@
(const GURL&),
(const, override));
MOCK_METHOD(bool, IsFillingEnabled, (const GURL&), (const, override));
+ MOCK_METHOD(const GURL&, GetLastCommittedURL, (), (const, override));
MOCK_METHOD(void,
AutofillHttpAuth,
(const PasswordForm&, const PasswordFormManagerForUI*),
@@ -447,6 +448,69 @@
httpauth_manager_.reset();
}
+#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+// The biometric reauth prompt should name the origin of the challenger.
+TEST_P(HttpAuthManagerTest, HttpAuthFillingReauthMessageUsesChallengerOrigin) {
+ EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true));
+ EXPECT_CALL(client_, IsReauthBeforeFillingRequired)
+ .WillRepeatedly(Return(true));
+ const GURL main_frame_url("http://embedder.com/");
+ ON_CALL(client_, GetLastCommittedURL)
+ .WillByDefault(ReturnRef(main_frame_url));
+ 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());
+ });
+
+ std::u16string message;
+ auto mock_authenticator =
+ std::make_unique<device_reauth::MockDeviceAuthenticator>();
+ EXPECT_CALL(*mock_authenticator, AuthenticateWithMessage)
+ .WillOnce(
+ [&](const std::u16string& msg,
+ device_reauth::DeviceAuthenticator::AuthenticateCallback cb) {
+ message = msg;
+ std::move(cb).Run(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(), password_manager::FromPasswordForms(std::move(result)));
+
+ EXPECT_NE(std::u16string::npos, message.find(u"proxy.com"));
+ EXPECT_EQ(std::u16string::npos, message.find(u"embedder.com"));
+
+ testing::Mock::VerifyAndClearExpectations(&store_);
+ httpauth_manager()->DetachObserver(&observer);
+}
+#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+
TEST_P(HttpAuthManagerTest, HttpAuthSaving) {
for (bool filling_and_saving_enabled : {true, false}) {
SCOPED_TRACE(testing::Message("filling_and_saving_enabled=")
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 a16f6b9..a4a6ee9 100644
--- a/components/password_manager/core/browser/http_auth_manager_unittest.cc
+++ b/components/password_manager/core/browser/http_auth_manager_unittest.cc
@@ -62,6 +62,7 @@
(const GURL&),
(const, override));
MOCK_METHOD(bool, IsFillingEnabled, (const GURL&), (const, override));
+ MOCK_METHOD(const GURL&, GetLastCommittedURL, (), (const, override));
MOCK_METHOD(void,
AutofillHttpAuth,
(const PasswordForm&, const PasswordFormManagerForUI*),
@@ -447,6 +448,69 @@
httpauth_manager_.reset();
}
+#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+// The biometric reauth prompt should name the origin of the challenger.
+TEST_P(HttpAuthManagerTest, HttpAuthFillingReauthMessageUsesChallengerOrigin) {
+ EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true));
+ EXPECT_CALL(client_, IsReauthBeforeFillingRequired)
+ .WillRepeatedly(Return(true));
+ const GURL main_frame_url("http://embedder.com/");
+ ON_CALL(client_, GetLastCommittedURL)
+ .WillByDefault(ReturnRef(main_frame_url));
+ 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());
+ });
+
+ std::u16string message;
+ auto mock_authenticator =
+ std::make_unique<device_reauth::MockDeviceAuthenticator>();
+ EXPECT_CALL(*mock_authenticator, AuthenticateWithMessage)
+ .WillOnce(
+ [&](const std::u16string& msg,
+ device_reauth::DeviceAuthenticator::AuthenticateCallback cb) {
+ message = msg;
+ std::move(cb).Run(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(), password_manager::FromPasswordForms(std::move(result)));
+
+ EXPECT_NE(std::u16string::npos, message.find(u"proxy.com"));
+ EXPECT_EQ(std::u16string::npos, message.find(u"embedder.com"));
+
+ testing::Mock::VerifyAndClearExpectations(&store_);
+ httpauth_manager()->DetachObserver(&observer);
+}
+#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+
TEST_P(HttpAuthManagerTest, HttpAuthSaving) {
for (bool filling_and_saving_enabled : {true, false}) {
SCOPED_TRACE(testing::Message("filling_and_saving_enabled=")
Original Bug Report
Potential origin-confusion in HTTP-auth biometric prompt displaying top-frame origin
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: When filling an HTTP-auth credential that requires biometric re-authentication on desktop platforms (macOS, Windows, and ChromeOS), the OS biometric prompt displays the top-level page origin instead of the subframe’s challenger origin. This creates a potential origin-confusion vulnerability in trusted OS UI, where a user may authorize password autofill believing it is for the top-level site when it is actually filled for a cross-origin iframe.
Affected files:
components/password_manager/core/browser/http_auth_manager_impl.ccchrome/browser/password_manager/chrome_password_manager_client.cc
Estimated timestamp from git blame: 2025-10-23
Root Cause
In HttpAuthManagerImpl::Autofill(), the message shown in the OS biometric prompt (Touch ID, Windows Hello, or ChromeOS fingerprint) is built using the per-WebContents client’s last-committed URL (which returns the primary main-frame URL) rather than the HTTP-auth challenger origin to which the credential is keyed.
Specifically, in components/password_manager/core/browser/http_auth_manager_impl.cc around line 120:
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
authenticator_->AuthenticateWithMessage(message, std::move(filling_callback));
client_ is an instance of ChromePasswordManagerClient (one per WebContents), and its GetLastCommittedURL() method returns the top-level main frame’s URL (see chrome/browser/password_manager/chrome_password_manager_client.cc around line 1144):
const GURL& ChromePasswordManagerClient::GetLastCommittedURL() const {
return web_contents()->GetLastCommittedURL();
}
Meanwhile, the target credential and form_manager_ are bound to the challenger origin (e.g., https://attacker.example). The challenger origin is available within Autofill() as url::Origin::Create(preferred_match.url) and form_manager_->GetURL(), but is not utilized for the prompt message construction.
Potential Impact
This is a potential CWE-346 origin-confusion vulnerability in highly trusted OS-rendered UI (such as macOS Touch ID or Windows Hello prompts). Because the prompt text is derived from the top-frame origin rather than the bound credential, the OS dialog informs the user “Google Chrome is trying to fill your password on victim.example” while the credential queued for filling is actually for attacker.example and will be delivered to a LoginView that submits to attacker.example.
Note that this does not allow direct cross-origin credential theft (e.g., filling a victim.example credential on attacker.example) because OnReauthCompleted() contains post-biometric checks that verify the credential origin against the observed/challenger origin. However, it completely misrepresents which site is receiving the autofilled credentials within the OS security prompt.
Potential Steps to Trigger (unverified by automated execution)
- A user enables “Use [Touch ID / Windows Hello / fingerprint] when filling passwords” in
chrome://password-manager/settings. - The user has a saved HTTP Basic-auth credential for
https://attacker.example. - The user navigates to
https://victim.example, which embeds a cross-origin subframe pointing tohttps://attacker.example/protected. - The subframe returns a
401 WWW-Authenticatechallenge. - Chrome’s login delegate flow is triggered, and
PasswordFormManagerfetches the saved credential forhttps://attacker.example. HttpAuthManagerImpl::Autofill()is invoked. The prompt origin is derived fromweb_contents()->GetLastCommittedURL()(which ishttps://victim.example).- The OS modal displays “Google Chrome is trying to fill your password on victim.example” while the actual credential being released is for
attacker.example. - Upon biometric authorization, the saved
attacker.examplecredential is auto-filled and sent to the attacker domain, despite the OS prompt claiming it was forvictim.example.
Suggested Fix
Update HttpAuthManagerImpl::Autofill() in components/password_manager/core/browser/http_auth_manager_impl.cc to construct the prompt message using the origin of the matched credential/form rather than client_->GetLastCommittedURL():
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
const std::u16string origin = base::UTF8ToUTF16(
GetShownOrigin(url::Origin::Create(preferred_match.url)));
message =
l10n_util::GetStringFUTF16(IDS_PASSWORD_MANAGER_FILLING_REAUTH, origin);
#endif
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.