Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient data validation in Password Manager
DescriptionInsufficient data validation in Password Manager
ComponentPassword Manager
Bug ClassLogic Error
Tracker497831111
Fix commitee47b0994e1d (chromium/src) +158/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
components/password_manager/core/browser/http_auth_manager_impl.cc
modified
DeviceAuthenticator
components/password_manager/core/browser/http_auth_manager_impl.h
modified
TEST_P
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_impl.h
  • components/password_manager/core/browser/http_auth_manager_unittest.cc
From ee47b0994e1d284cb19260527bc92d42b6307c7a Mon Sep 17 00:00:00 2001
From: Viktor Semeniuk <vsemeniuk@google.com>
Date: Fri, 10 Apr 2026 07:48:08 -0700
Subject: [PATCH] Verify origin before filling the form during http auth

This CL adds verification for origin before performing filling password
after biometric authentication. Also if SetObserverAndDeliverCredentials
is invoked any ongoing authentication is canceled.

Fixed: 497831111
Change-Id: I72e03e523c6641f9fe1c58b4b0a8f24c63d91a22
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7748326
Commit-Queue: Viktor Semeniuk <vsemeniuk@google.com>
Reviewed-by: Vasilii Sukhanov <vasilii@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1612861}
---

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 f81ab875..e242256b 100644
--- a/components/password_manager/core/browser/http_auth_manager_impl.cc
+++ b/components/password_manager/core/browser/http_auth_manager_impl.cc
@@ -62,11 +62,15 @@
   if (observer_) {
     observer_->OnLoginModelDestroying();
   }
+  if (authenticator_) {
+    authenticator_->Cancel();
+  }
   observer_ = observer;
 
   if (!client_->IsFillingEnabled(observed_form.url)) {
     return;
   }
+  observed_origin_ = url::Origin::Create(observed_form.url);
   // Initialize the form manager.
   form_manager_ = std::make_unique<PasswordFormManager>(
       client_, PasswordFormDigest(observed_form), nullptr /* form_fetcher */,
@@ -107,7 +111,7 @@
   auto filling_callback = base::BindOnce(
       &HttpAuthManagerImpl::OnReauthCompleted, weak_ptr_factory_.GetWeakPtr(),
       preferred_match.username_value, preferred_match.password_value,
-      std::move(on_filling_complete));
+      url::Origin::Create(preferred_match.url), std::move(on_filling_complete));
   if (authenticator_) {
     authenticator_->Cancel();
   }
@@ -126,9 +130,17 @@
 void HttpAuthManagerImpl::OnReauthCompleted(
     const std::u16string& username,
     const std::u16string& password,
+    const url::Origin& credential_origin,
     base::OnceClosure on_filling_complete,
     bool auth_result) {
   if (observer_ && auth_result) {
+    if (observed_origin_ != credential_origin) {
+      return;
+    }
+    if (form_manager_ &&
+        url::Origin::Create(form_manager_->GetURL()) != credential_origin) {
+      return;
+    }
     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 9c487b0d..7a52c82 100644
--- a/components/password_manager/core/browser/http_auth_manager_impl.h
+++ b/components/password_manager/core/browser/http_auth_manager_impl.h
@@ -11,6 +11,7 @@
 #include "components/password_manager/core/browser/browser_save_password_progress_logger.h"
 #include "components/password_manager/core/browser/http_auth_manager.h"
 #include "components/password_manager/core/browser/http_auth_observer.h"
+#include "url/origin.h"
 
 namespace device_reauth {
 class DeviceAuthenticator;
@@ -68,6 +69,7 @@
 
   void OnReauthCompleted(const std::u16string& username,
                          const std::u16string& password,
+                         const url::Origin& credential_origin,
                          base::OnceClosure on_filling_complete,
                          bool auth_result);
 
@@ -84,6 +86,9 @@
   // will be cleared on next navigation.
   bool form_dismissed_ = false;
 
+  // The origin of the observed form.
+  url::Origin observed_origin_;
+
   // The authenticator used to trigger a biometric re-auth before filling.
   std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator_;
 
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 04c28cf5..ec20e14b 100644
--- a/components/password_manager/core/browser/http_auth_manager_unittest.cc
+++ b/components/password_manager/core/browser/http_auth_manager_unittest.cc
@@ -305,6 +305,146 @@
   httpauth_manager()->DetachObserver(&observer);
 }
 
+// Test autofill when biometric re-auth is required and successful but origin
+// mismatches.
+TEST_P(HttpAuthManagerTest, HttpAuthFillingReauthOriginMismatch) {
+  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) {
+            // Modify the match to have a different origin.
+            PasswordForm mismatched_match = preferred_match;
+            mismatched_match.url = GURL("http://different.com/");
+            httpauth_manager()->Autofill(mismatched_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(*mock_authenticator, Cancel).Times(testing::AtLeast(1));
+  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);
+}
+
+TEST_P(HttpAuthManagerTest, CrossOriginLeakViaStaleBiometricObserver) {
+  EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true));
+  EXPECT_CALL(client_, IsReauthBeforeFillingRequired)
+      .WillRepeatedly(Return(true));
+
+  base::MockOnceClosure mock_filling_complete;
+  EXPECT_CALL(client_, AutofillHttpAuth)
+      .WillRepeatedly(
+          [&](const password_manager::PasswordForm& preferred_match,
+              const password_manager::PasswordFormManagerForUI* form_manager) {
+            httpauth_manager()->Autofill(preferred_match, form_manager,
+                                         mock_filling_complete.Get());
+          });
+
+  device_reauth::DeviceAuthenticator::AuthenticateCallback pending_auth_cb;
+  auto mock_authenticator =
+      std::make_unique<device_reauth::MockDeviceAuthenticator>();
+  device_reauth::MockDeviceAuthenticator* authenticator_ptr =
+      mock_authenticator.get();
+  EXPECT_CALL(*authenticator_ptr, AuthenticateWithMessage)
+      .WillOnce(
+          [&](const std::u16string&,
+              device_reauth::DeviceAuthenticator::AuthenticateCallback cb) {
+            pending_auth_cb = std::move(cb);
+          });
+
+  // The pending authenticator MUST be cancelled when the observer changes.
+  EXPECT_CALL(*authenticator_ptr, Cancel).Times(testing::AtLeast(1));
+
+  EXPECT_CALL(client_, GetDeviceAuthenticator)
+      .WillOnce(Return(testing::ByMove(std::move(mock_authenticator))));
+
+  // Step 1: First HTTP-auth challenge from victim.com
+  PasswordForm victim_form;
+  victim_form.scheme = PasswordForm::Scheme::kBasic;
+  victim_form.url = GURL("https://victim.example/");
+  victim_form.signon_realm = "https://victim.example/realm";
+
+  PasswordForm victim_creds = victim_form;
+  victim_creds.username_value = u"victim_user";
+  victim_creds.password_value = u"victim_secret";
+
+  testing::StrictMock<MockHttpAuthObserver> victim_observer;
+
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 04c28cf5..ec20e14b 100644
--- a/components/password_manager/core/browser/http_auth_manager_unittest.cc
+++ b/components/password_manager/core/browser/http_auth_manager_unittest.cc
@@ -305,6 +305,146 @@
   httpauth_manager()->DetachObserver(&observer);
 }
 
+// Test autofill when biometric re-auth is required and successful but origin
+// mismatches.
+TEST_P(HttpAuthManagerTest, HttpAuthFillingReauthOriginMismatch) {
+  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) {
+            // Modify the match to have a different origin.
+            PasswordForm mismatched_match = preferred_match;
+            mismatched_match.url = GURL("http://different.com/");
+            httpauth_manager()->Autofill(mismatched_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(*mock_authenticator, Cancel).Times(testing::AtLeast(1));
+  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);
+}
+
+TEST_P(HttpAuthManagerTest, CrossOriginLeakViaStaleBiometricObserver) {
+  EXPECT_CALL(client_, IsFillingEnabled).WillRepeatedly(Return(true));
+  EXPECT_CALL(client_, IsReauthBeforeFillingRequired)
+      .WillRepeatedly(Return(true));
+
+  base::MockOnceClosure mock_filling_complete;
+  EXPECT_CALL(client_, AutofillHttpAuth)
+      .WillRepeatedly(
+          [&](const password_manager::PasswordForm& preferred_match,
+              const password_manager::PasswordFormManagerForUI* form_manager) {
+            httpauth_manager()->Autofill(preferred_match, form_manager,
+                                         mock_filling_complete.Get());
+          });
+
+  device_reauth::DeviceAuthenticator::AuthenticateCallback pending_auth_cb;
+  auto mock_authenticator =
+      std::make_unique<device_reauth::MockDeviceAuthenticator>();
+  device_reauth::MockDeviceAuthenticator* authenticator_ptr =
+      mock_authenticator.get();
+  EXPECT_CALL(*authenticator_ptr, AuthenticateWithMessage)
+      .WillOnce(
+          [&](const std::u16string&,
+              device_reauth::DeviceAuthenticator::AuthenticateCallback cb) {
+            pending_auth_cb = std::move(cb);
+          });
+
+  // The pending authenticator MUST be cancelled when the observer changes.
+  EXPECT_CALL(*authenticator_ptr, Cancel).Times(testing::AtLeast(1));
+
+  EXPECT_CALL(client_, GetDeviceAuthenticator)
+      .WillOnce(Return(testing::ByMove(std::move(mock_authenticator))));
+
+  // Step 1: First HTTP-auth challenge from victim.com
+  PasswordForm victim_form;
+  victim_form.scheme = PasswordForm::Scheme::kBasic;
+  victim_form.url = GURL("https://victim.example/");
+  victim_form.signon_realm = "https://victim.example/realm";
+
+  PasswordForm victim_creds = victim_form;
+  victim_creds.username_value = u"victim_user";
+  victim_creds.password_value = u"victim_secret";
+
+  testing::StrictMock<MockHttpAuthObserver> victim_observer;
+
+  base::WeakPtr<PasswordStoreConsumer> victim_consumer;
+  EXPECT_CALL(*store_, GetLogins(PasswordFormDigest(victim_form), _))
+      .WillOnce(SaveArg<1>(&victim_consumer));
+  httpauth_manager()->SetObserverAndDeliverCredentials(&victim_observer,
+                                                       victim_form);
+  ASSERT_TRUE(victim_consumer);
+  std::vector<PasswordForm> victim_result;
+  victim_result.push_back(victim_creds);
+  victim_consumer->OnGetPasswordStoreResultsOrErrorFrom(
+      store_.get(), std::move(victim_result));
+  // Biometric prompt is now armed and pending.
+  ASSERT_FALSE(pending_auth_cb.is_null());
+
+  // Step 2: Second HTTP-auth challenge from evil.com
+  PasswordForm evil_form;
+  evil_form.scheme = PasswordForm::Scheme::kBasic;
+  evil_form.url = GURL("https://evil.example/");
+  evil_form.signon_realm = "https://evil.example/realm";
+
+  testing::StrictMock<MockHttpAuthObserver> evil_observer;
+
+  EXPECT_CALL(victim_observer, OnLoginModelDestroying);
+  EXPECT_CALL(*store_, GetLogins(PasswordFormDigest(evil_form), _))
+      .WillOnce(WithArg<1>(InvokeEmptyConsumerWithForms(store_.get())));
+
+  // This call should trigger Cancel() on the authenticator.
+  httpauth_manager()->SetObserverAndDeliverCredentials(&evil_observer,
+                                                       evil_form);
+
+  // Step 3: User completes the biometric prompt (simulated)
+  EXPECT_CALL(victim_observer, OnAutofillDataAvailable).Times(0);
+  EXPECT_CALL(evil_observer, OnAutofillDataAvailable).Times(0);
+  EXPECT_CALL(mock_filling_complete, Run).Times(0);
+
+  std::move(pending_auth_cb).Run(/*auth_result=*/true);
+
+  testing::Mock::VerifyAndClearExpectations(authenticator_ptr);
+  testing::Mock::VerifyAndClearExpectations(store_.get());
+  httpauth_manager()->DetachObserver(&evil_observer);
+  httpauth_manager_.reset();
+}
+
 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 rj...@google.com

Potential cross-origin credential disclosure via stale biometric re-authentication observer

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A vulnerability in HttpAuthManagerImpl allows a cross-origin credential leak when biometric re-authentication is enabled. If a second authentication challenge occurs while a biometric prompt is pending, the first site’s credentials can be delivered to the second site’s login dialog. An attacker can use this to exfiltrate saved HTTP Basic or Digest credentials.

Affected files:

  • components/password_manager/core/browser/http_auth_manager_impl.cc
  • chrome/browser/ui/views/login_view.cc
  • chrome/browser/password_manager/chrome_password_manager_client.cc

Estimated timestamp from git blame: 2024-05-22

Summary

A potential vulnerability exists in HttpAuthManagerImpl that can lead to the disclosure of HTTP Basic/Digest credentials across origins when the kBiometricAuthenticationBeforeFilling preference is enabled. HttpAuthManagerImpl is a per-WebContents singleton that manages HTTP authentication challenges within a tab. When a biometric re-authentication prompt is pending for one site, a second concurrent challenge from a different site can overwrite the active observer without cancelling the pending authentication. Upon successful biometric validation, the original site’s credentials are delivered to the second site’s login dialog.

Technical Analysis

When an HTTP authentication challenge occurs and biometric re-authentication is required, HttpAuthManagerImpl::Autofill binds the site’s saved credentials into an asynchronous callback, OnReauthCompleted. The authenticator is then armed to show the biometric prompt.

In components/password_manager/core/browser/http_auth_manager_impl.cc, the SetObserverAndDeliverCredentials method handles the registration of a LoginView as an observer for the authentication process:

void HttpAuthManagerImpl::SetObserverAndDeliverCredentials(
    HttpAuthObserver* observer,
    const PasswordForm& observed_form) {
  // ...
  if (observer_) {
    observer_->OnLoginModelDestroying();
  }
  observer_ = observer;
  // ...
}

If a second HTTP-auth challenge arrives from a different origin while the biometric prompt for the first site is still active, SetObserverAndDeliverCredentials() is called again. It replaces the existing observer_ (the first site’s LoginView) with the new site’s LoginView. However, it fails to call authenticator_->Cancel() to invalidate the pending biometric request.

If the second origin has no saved credentials, the code path that would normally call Autofill() (which includes an authenticator_->Cancel() call) is skipped. Consequently, the original authenticator_ remains active, and its success callback is still bound by value to the first site’s credentials.

When the user completes the biometric authentication, OnReauthCompleted() is executed:

void HttpAuthManagerImpl::OnReauthCompleted(
    const std::u16string& username,
    const std::u16string& password,
    base::OnceClosure on_filling_complete,
    bool auth_result) {
  if (observer_ && auth_result) {
    observer_->OnAutofillDataAvailable(username, password);
    std::move(on_filling_complete).Run();
  }
}

Because observer_ now points to the second site’s LoginView, OnAutofillDataAvailable is called on the attacker-controlled dialog with the victim’s credentials from the first site. If the user clicks “Sign In” on this pre-filled dialog, the credentials are sent to the attacker’s server in the Authorization header.

Potential Steps to Reproduce

Note: These are potential steps as our tooling agent doesn’t yet have the ability to run code to confirm them end-to-end.

  1. Enable “Use Windows Hello / Touch ID when filling passwords” in chrome://settings/passwords (or the equivalent setting on Android/Mac).
  2. Ensure HTTP Basic credentials are saved for a target site (e.g., https://victim-bank.com).
  3. The attacker site (https://evil.com) must have no saved credentials in the manager.
  4. The victim visits evil.com.
  5. evil.com triggers a cross-origin fetch() request to victim-bank.com that returns a 401 challenge. This prompts the biometric re-auth sheet.
  6. While the biometric prompt is active, evil.com triggers a second fetch() request to its own origin (evil.com) that returns a 401 challenge. This overwrites the observer in HttpAuthManagerImpl.
  7. The user completes the biometric authentication.
  8. evil.com aborts the first fetch() request, dismissing the first dialog (or the user cancels it).
  9. The attacker’s evil.com login dialog will appear, pre-filled with the credentials for victim-bank.com.
  10. If the user clicks “Sign In”, the victim’s credentials are sent to evil.com.

Suggested Fix

In HttpAuthManagerImpl::SetObserverAndDeliverCredentials, if authenticator_ is active when the observer is being changed, it must be explicitly cancelled.

void HttpAuthManagerImpl::SetObserverAndDeliverCredentials(
    HttpAuthObserver* observer,
    const PasswordForm& observed_form) {
  // ...
  if (observer_) {
    observer_->OnLoginModelDestroying();
  }
  if (authenticator_) {
    authenticator_->Cancel();
  }
  observer_ = observer;
  // ...
}

Alternatively, OnReauthCompleted could verify that the form_manager_’s URL or the observer_ matches the origin of the credentials it is about to deliver.

Evaluated with Chrome root at commit: 876d480da1f794d87813cfa2e6ff4fcf9771e939


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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