Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactIncorrect security UI in Passwords
DescriptionIncorrect security UI in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker518082162
Fix commitdbadf35b644f (chromium/src) +65/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
TEST_P
components/password_manager/core/browser/http_auth_manager_unittest.cc
modified
for
components/password_manager/core/browser/http_auth_manager_unittest.cc
modified

Files Changed

  • components/password_manager/core/browser/http_auth_manager_impl.cc
  • components/password_manager/core/browser/http_auth_manager_unittest.cc
From dbadf35b644fbcb6ed308419043bf0392dbe1074 Mon Sep 17 00:00:00 2001
From: Viktor Semeniuk <[email protected]>
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 <[email protected]>
Reviewed-by: Ioana Treib <[email protected]>
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=")
Loading diff…

Regression Test / PoC

shipped with the fix
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=")
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chrome/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)

  1. A user enables “Use [Touch ID / Windows Hello / fingerprint] when filling passwords” in chrome://password-manager/settings.
  2. The user has a saved HTTP Basic-auth credential for https://attacker.example.
  3. The user navigates to https://victim.example, which embeds a cross-origin subframe pointing to https://attacker.example/protected.
  4. The subframe returns a 401 WWW-Authenticate challenge.
  5. Chrome’s login delegate flow is triggered, and PasswordFormManager fetches the saved credential for https://attacker.example.
  6. HttpAuthManagerImpl::Autofill() is invoked. The prompt origin is derived from web_contents()->GetLastCommittedURL() (which is https://victim.example).
  7. 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.
  8. Upon biometric authorization, the saved attacker.example credential is auto-filled and sent to the attacker domain, despite the OS prompt claiming it was for victim.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.

View on issue tracker