Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Passwords
DescriptionInsufficient policy enforcement in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker517675979
Fix commit323927c81c17 (chromium/src) +133/-196
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
chrome/browser/password_manager/android/account_chooser_dialog_android.cc
modified
MockPasswordManagerClient
chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
modified
AccountChooserDialogAndroidTest
chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
modified
TEST_F
chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
modified

Files Changed

  • chrome/browser/password_manager/android/account_chooser_dialog_android.cc
  • chrome/browser/password_manager/android/account_chooser_dialog_android.h
  • chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
From 323927c81c1702c6d111450afd87c5faf1f795af Mon Sep 17 00:00:00 2001
From: Anna Tsvirchkova <atsvirchkova@google.com>
Date: Thu, 18 Jun 2026 08:47:55 -0700
Subject: [PATCH] Refactor biometric reauth in credential API

This CL also adds the possibility for biometric auth on Android. It also
adds canceling biometric auth on destruction of CredentialManagerImpl.

Bug: 517675979
Change-Id: Ia2bb654c13f299ec87a5e7e878dced8e36acef30
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7952717
Reviewed-by: Ioana Treib <ioanap@chromium.org>
Commit-Queue: Anna Tsvirchkova <atsvirchkova@google.com>
Cr-Commit-Position: refs/heads/main@{#1649077}
---

diff --git a/chrome/browser/password_manager/android/account_chooser_dialog_android.cc b/chrome/browser/password_manager/android/account_chooser_dialog_android.cc
index c90ee3b..e19d01ce 100644
--- a/chrome/browser/password_manager/android/account_chooser_dialog_android.cc
+++ b/chrome/browser/password_manager/android/account_chooser_dialog_android.cc
@@ -16,7 +16,6 @@
 #include "chrome/browser/ui/passwords/account_avatar_fetcher.h"
 #include "chrome/browser/ui/passwords/ui_utils.h"
 #include "chrome/grit/generated_resources.h"
-#include "components/device_reauth/device_authenticator.h"
 #include "components/password_manager/core/browser/password_manager_constants.h"
 #include "components/password_manager/core/browser/password_manager_metrics_util.h"
 #include "components/password_manager/core/browser/password_ui_utils.h"
@@ -117,10 +116,6 @@
 }
 
 AccountChooserDialogAndroid::~AccountChooserDialogAndroid() {
-  if (authenticator_) {
-    authenticator_->Cancel();
-  }
-
   // |dialog_jobject_| can be null in tests or if the dialog could not
   // be shown.
   if (dialog_jobject_) {
@@ -190,11 +185,7 @@
     return;
   }
 
-  // If an authentication is in progress, the user already selected a
-  // credential so the dialog action should not be marked as cancel.
-  if (!authenticator_) {
-    OnDialogCancel();
-  }
+  OnDialogCancel();
   delete this;
 }
 
@@ -216,32 +207,8 @@
     return true;
   }
 
-  std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator =
-      client_->GetDeviceAuthenticator();
-  if (client_->IsReauthBeforeFillingRequired(authenticator.get())) {
-    authenticator_ = std::move(authenticator);
-    authenticator_->AuthenticateWithMessage(
-        u"", base::BindOnce(&AccountChooserDialogAndroid::OnReauthCompleted,
-                            base::Unretained(this), index));
-    // The credential handling will only happen after the authentication
-    // finishes.
-    return false;
-  }
-
   passwords_data_.ChooseCredential(credentials_forms[index].get());
   return true;
 }
 
-void AccountChooserDialogAndroid::OnReauthCompleted(size_t index,
-                                                    bool auth_succeeded) {
-  authenticator_.reset();
-  if (auth_succeeded) {
-    const auto& credentials_forms = local_credentials_forms();
-    passwords_data_.ChooseCredential(credentials_forms[index].get());
-  } else {
-    passwords_data_.ChooseCredential(nullptr);
-  }
-  delete this;
-}
-
 DEFINE_JNI(AccountChooserDialog)
diff --git a/chrome/browser/password_manager/android/account_chooser_dialog_android.h b/chrome/browser/password_manager/android/account_chooser_dialog_android.h
index abcb64b..21bfaf68 100644
--- a/chrome/browser/password_manager/android/account_chooser_dialog_android.h
+++ b/chrome/browser/password_manager/android/account_chooser_dialog_android.h
@@ -9,11 +9,8 @@
 
 #include <vector>
 
-// #include "base/android/jni_android.h"
 #include "base/memory/raw_ptr.h"
 #include "chrome/browser/ui/passwords/manage_passwords_state.h"
-#include "components/device_reauth/device_authenticator.h"
-#include "components/password_manager/core/browser/password_manager_metrics_util.h"
 #include "content/public/browser/web_contents_observer.h"
 
 namespace content {
@@ -66,20 +63,11 @@
   // required, the handling is not considered done until that finishes.
   bool HandleCredentialChosen(size_t index, bool sign_button_clicked);
 
-  // Called when the biometric re-auth finished. |index| is the index
-  // of the chosen credential and |auth_succeeded| is the result of the
-  // re-authentication. Destroys |this|.
-  void OnReauthCompleted(size_t index, bool auth_succeded);
-
   raw_ptr<content::WebContents> web_contents_ = nullptr;
 
   // Client used to retrieve the biometric authenticator.
   raw_ptr<password_manager::PasswordManagerClient> client_ = nullptr;
 
-  // Authenticator used to trigger a biometric re-auth before passing the
-  // credential to the site.
-  std::unique_ptr<device_reauth::DeviceAuthenticator> authenticator_;
-
   ManagePasswordsState passwords_data_;
   url::Origin origin_;
   base::android::ScopedJavaGlobalRef<jobject> dialog_jobject_;
diff --git a/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc b/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
index 605d47e..20d85099 100644
--- a/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
+++ b/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
@@ -4,15 +4,10 @@
 
 #include "chrome/browser/password_manager/android/account_chooser_dialog_android.h"
 
-#include "base/android/device_info.h"
-#include "base/test/gmock_callback_support.h"
 #include "base/test/mock_callback.h"
 #include "chrome/browser/password_manager/chrome_password_manager_client.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
-#include "components/device_reauth/device_authenticator.h"
-#include "components/device_reauth/mock_device_authenticator.h"
-#include "components/password_manager/core/browser/features/password_features.h"
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_manager_metrics_util.h"
 #include "components/password_manager/core/browser/password_manager_test_utils.h"
@@ -23,12 +18,7 @@
 
 namespace {
 
-using base::test::RunOnceCallback;
-using device_reauth::MockDeviceAuthenticator;
-using testing::_;
-using testing::Eq;
 using testing::Pointee;
-using testing::Return;
 
 password_manager::PasswordFormData kFormData1 = {
     password_manager::PasswordForm::Scheme::kHtml,
@@ -59,17 +49,7 @@
 };
 
 class MockPasswordManagerClient
-    : public password_manager::StubPasswordManagerClient {
- public:
-  MOCK_METHOD(std::unique_ptr<device_reauth::DeviceAuthenticator>,
-              GetDeviceAuthenticator,
-              (),
-              (override));
-  MOCK_METHOD(bool,
-              IsReauthBeforeFillingRequired,
-              (device_reauth::DeviceAuthenticator*),
-              (override));
-};
+    : public password_manager::StubPasswordManagerClient {};
 
 }  // namespace
 
@@ -97,14 +77,9 @@
   base::MockCallback<ManagePasswordsState::CredentialsCallback>
       credential_callback_;
 
- private:
-  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
-AccountChooserDialogAndroidTest::AccountChooserDialogAndroidTest() {
-  scoped_feature_list_.InitAndEnableFeature(
-      password_manager::features::kBiometricTouchToFill);
-}
+AccountChooserDialogAndroidTest::AccountChooserDialogAndroidTest() = default;
 
 void AccountChooserDialogAndroidTest::SetUp() {
   ChromeRenderViewHostTestHarness::SetUp();
@@ -128,19 +103,9 @@
   return CreateDialog(std::move(credentials));
 }
 
-TEST_F(AccountChooserDialogAndroidTest, SendsCredentialIfAuthNotAvailable) {
-  // Auth is required to fill passwords in Android automotive.
-  if (base::android::device_info::is_automotive()) {
-    GTEST_SKIP();
-  }
-
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc b/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
index 605d47e..20d85099 100644
--- a/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
+++ b/chrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
@@ -4,15 +4,10 @@
 
 #include "chrome/browser/password_manager/android/account_chooser_dialog_android.h"
 
-#include "base/android/device_info.h"
-#include "base/test/gmock_callback_support.h"
 #include "base/test/mock_callback.h"
 #include "chrome/browser/password_manager/chrome_password_manager_client.h"
 #include "chrome/browser/profiles/profile.h"
 #include "chrome/test/base/chrome_render_view_host_test_harness.h"
-#include "components/device_reauth/device_authenticator.h"
-#include "components/device_reauth/mock_device_authenticator.h"
-#include "components/password_manager/core/browser/features/password_features.h"
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_manager_metrics_util.h"
 #include "components/password_manager/core/browser/password_manager_test_utils.h"
@@ -23,12 +18,7 @@
 
 namespace {
 
-using base::test::RunOnceCallback;
-using device_reauth::MockDeviceAuthenticator;
-using testing::_;
-using testing::Eq;
 using testing::Pointee;
-using testing::Return;
 
 password_manager::PasswordFormData kFormData1 = {
     password_manager::PasswordForm::Scheme::kHtml,
@@ -59,17 +49,7 @@
 };
 
 class MockPasswordManagerClient
-    : public password_manager::StubPasswordManagerClient {
- public:
-  MOCK_METHOD(std::unique_ptr<device_reauth::DeviceAuthenticator>,
-              GetDeviceAuthenticator,
-              (),
-              (override));
-  MOCK_METHOD(bool,
-              IsReauthBeforeFillingRequired,
-              (device_reauth::DeviceAuthenticator*),
-              (override));
-};
+    : public password_manager::StubPasswordManagerClient {};
 
 }  // namespace
 
@@ -97,14 +77,9 @@
   base::MockCallback<ManagePasswordsState::CredentialsCallback>
       credential_callback_;
 
- private:
-  base::test::ScopedFeatureList scoped_feature_list_;
 };
 
-AccountChooserDialogAndroidTest::AccountChooserDialogAndroidTest() {
-  scoped_feature_list_.InitAndEnableFeature(
-      password_manager::features::kBiometricTouchToFill);
-}
+AccountChooserDialogAndroidTest::AccountChooserDialogAndroidTest() = default;
 
 void AccountChooserDialogAndroidTest::SetUp() {
   ChromeRenderViewHostTestHarness::SetUp();
@@ -128,19 +103,9 @@
   return CreateDialog(std::move(credentials));
 }
 
-TEST_F(AccountChooserDialogAndroidTest, SendsCredentialIfAuthNotAvailable) {
-  // Auth is required to fill passwords in Android automotive.
-  if (base::android::device_info::is_automotive()) {
-    GTEST_SKIP();
-  }
-
+TEST_F(AccountChooserDialogAndroidTest, SendsCredentialClick) {
   AccountChooserDialogAndroid* dialog = CreateDialogManyAccounts();
 
-  auto authenticator = std::make_unique<MockDeviceAuthenticator>();
-
-  EXPECT_CALL(client_, IsReauthBeforeFillingRequired).WillOnce(Return(false));
-  EXPECT_CALL(client_, GetDeviceAuthenticator)
-      .WillOnce(Return(testing::ByMove(std::move(authenticator))));
   std::unique_ptr<password_manager::PasswordForm> form =
       FillPasswordFormWithData(kFormData2, /*is_account_store=*/false);
 
@@ -150,62 +115,3 @@
                               1 /* credential_item */,
                               false /* signin_button_clicked */);
 }
-
-TEST_F(AccountChooserDialogAndroidTest, SendsCredentialIfAuthSuccessful) {
-  AccountChooserDialogAndroid* dialog = CreateDialogManyAccounts();
-
-  auto authenticator = std::make_unique<MockDeviceAuthenticator>();
-
-  ON_CALL(client_, IsReauthBeforeFillingRequired).WillByDefault(Return(true));
-  EXPECT_CALL(*authenticator, AuthenticateWithMessage)
-      .WillOnce(RunOnceCallback<1>(true));
-  EXPECT_CALL(client_, GetDeviceAuthenticator)
-      .WillOnce(Return(testing::ByMove(std::move(authenticator))));
-
-  std::unique_ptr<password_manager::PasswordForm> form =
-      FillPasswordFormWithData(kFormData2, /*is_account_store=*/false);
-  EXPECT_CALL(credential_callback_, Run(Pointee(*form.get())));
-
-  dialog->OnCredentialClicked(base::android::AttachCurrentThread(),
-                              1 /* credential_item */,
-                              false /* signin_button_clicked */);
-}
-
-TEST_F(AccountChooserDialogAndroidTest, DoesntSendCredentialIfAuthFailed) {
-  AccountChooserDialogAndroid* dialog = CreateDialogManyAccounts();
-
-  auto authenticator = std::make_unique<MockDeviceAuthenticator>();
-
-  ON_CALL(client_, IsReauthBeforeFillingRequired).WillByDefault(Return(true));
-  EXPECT_CALL(*authenticator, AuthenticateWithMessage)
-      .WillOnce(RunOnceCallback<1>(false));
-  EXPECT_CALL(client_, GetDeviceAuthenticator)
-      .WillOnce(Return(testing::ByMove(std::move(authenticator))));
-
-  std::unique_ptr<password_manager::PasswordForm> form =
-      FillPasswordFormWithData(kFormData2, /*is_account_store=*/false);
-  EXPECT_CALL(credential_callback_, Run(nullptr));
-
-  dialog->OnCredentialClicked(base::android::AttachCurrentThread(),
-                              1 /* credential_item */,
-                              false /* signin_button_clicked */);
-}
-
-TEST_F(AccountChooserDialogAndroidTest, CancelsAuthIfDestroyed) {
-  AccountChooserDialogAndroid* dialog = CreateDialogManyAccounts();
-
-  auto authenticator = std::make_unique<MockDeviceAuthenticator>();
-  auto* authenticator_ptr = authenticator.get();
-
-  ON_CALL(client_, IsReauthBeforeFillingRequired).WillByDefault(Return(true));
-  EXPECT_CALL(*authenticator_ptr, AuthenticateWithMessage);
-  EXPECT_CALL(client_, GetDeviceAuthenticator)
-      .WillOnce(Return(testing::ByMove(std::move(authenticator))));
-
-  dialog->OnCredentialClicked(base::android::AttachCurrentThread(),
-                              1 /* credential_item */,
-                              false /* signin_button_clicked */);
-
-  EXPECT_CALL(*authenticator_ptr, Cancel());
-  dialog->OnVisibilityChanged(content::Visibility::HIDDEN);
-}
diff --git a/components/password_manager/core/browser/credential_manager_impl_unittest.cc b/components/password_manager/core/browser/credential_manager_impl_unittest.cc
index 7d2389b..df11b35d 100644
--- a/components/password_manager/core/browser/credential_manager_impl_unittest.cc
+++ b/components/password_manager/core/browser/credential_manager_impl_unittest.cc
@@ -117,6 +117,10 @@
               (),
               (override));
   MOCK_METHOD(bool, IsActorTaskActive, (), (override));
+  MOCK_METHOD(bool,
+              IsReauthBeforeFillingRequired,
+              (device_reauth::DeviceAuthenticator*),
+              (override));
 
   explicit MockPasswordManagerClient(PasswordStoreInterface* profile_store,
                                      PasswordStoreInterface* account_store)
@@ -2031,9 +2035,7 @@
 #endif  // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
 
 TEST_P(CredentialManagerImplTest, ReauthAfterAccountSelection) {
-  ON_CALL(*client_->GetPasswordFeatureManager(),
-          IsBiometricAuthenticationBeforeFillingEnabled)
-      .WillByDefault(Return(true));
+  ON_CALL(*client_, IsReauthBeforeFillingRequired).WillByDefault(Return(true));
 
   store_->AddLogin(password_manager::FromPasswordForm(form_));
 
@@ -2044,6 +2046,7 @@
   device_reauth::MockDeviceAuthenticator* raw_authenticator =
       mock_authenticator.get();
   EXPECT_CALL(*client_, GetDeviceAuthenticator)
+      .WillOnce(Return(std::unique_ptr<device_reauth::DeviceAuthenticator>()))
       .WillOnce(Return(std::move(mock_authenticator)));
 
   EXPECT_CALL(*raw_authenticator, AuthenticateWithMessage)
@@ -2067,4 +2070,63 @@
   EXPECT_EQ(form_.username_value, credential->id);
 }
 
+TEST_P(CredentialManagerImplTest, ReauthBeforeSilentCredentialRetrieval) {
+  ON_CALL(*client_, IsReauthBeforeFillingRequired).WillByDefault(Return(true));
+
+  store_->AddLogin(password_manager::FromPasswordForm(form_));
+
+  // If reauth is required, silent credential retrieval should fail/return
+  // empty.
+  EXPECT_CALL(*client_, PromptUserToChooseCredentialsPtr).Times(0);
+  EXPECT_CALL(*client_, GetDeviceAuthenticator);
+  EXPECT_CALL(*client_, IsReauthBeforeFillingRequired);
+
+  bool called = false;
+  CredentialManagerError error;
+  std::optional<CredentialInfo> credential;
+  CallGet(CredentialMediationRequirement::kSilent, /*include_passwords=*/true,
+          /*federations=*/{},
+          base::BindOnce(&GetCredentialCallback, &called, &error, &credential));
+
+  RunAllPendingTasks();
+
+  EXPECT_TRUE(called);
+  EXPECT_EQ(CredentialManagerError::SUCCESS, error);
+  EXPECT_EQ(CredentialType::CREDENTIAL_TYPE_EMPTY, credential->type);
+}
+
+TEST_P(CredentialManagerImplTest, DestructionCancelsOngoingReauth) {
+  ON_CALL(*client_, IsReauthBeforeFillingRequired).WillByDefault(Return(true));
+
+  store_->AddLogin(password_manager::FromPasswordForm(form_));
+
+  auto mock_authenticator =
+      std::make_unique<device_reauth::MockDeviceAuthenticator>();
+  device_reauth::MockDeviceAuthenticator* raw_authenticator =
+      mock_authenticator.get();
+
+  EXPECT_CALL(*client_, GetDeviceAuthenticator)
+      // For CredentialManagerPendingRequestTask::ProcessForms re-auth
+      // availability check.
+      .WillOnce(Return(std::unique_ptr<device_reauth::DeviceAuthenticator>()))
+      // For CredentialManagerImpl::SendPasswordForm, which is under testing.
+      .WillOnce(Return(std::move(mock_authenticator)));
+
+  EXPECT_CALL(*raw_authenticator, AuthenticateWithMessage);
+  EXPECT_CALL(*raw_authenticator, Cancel);
+
+  bool called = false;
+  CredentialManagerError error;
+  std::optional<CredentialInfo> credential;
+  CallGet(CredentialMediationRequirement::kOptional, /*include_passwords=*/true,
+          /*federations=*/{},
+          base::BindOnce(&GetCredentialCallback, &called, &error, &credential));
+
+  RunAllPendingTasks();
+
+  cm_service_impl_.reset();
+
+  EXPECT_FALSE(called);
+}
+
 }  // namespace password_manager
diff --git a/components/password_manager/core/browser/credential_manager_pending_request_task_unittest.cc b/components/password_manager/core/browser/credential_manager_pending_request_task_unittest.cc
index 0c24f33..61f45651 100644
--- a/components/password_manager/core/browser/credential_manager_pending_request_task_unittest.cc
+++ b/components/password_manager/core/browser/credential_manager_pending_request_task_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/memory/raw_ptr.h"
 #include "base/strings/utf_string_conversions.h"
 #include "base/test/task_environment.h"
+#include "components/device_reauth/device_authenticator.h"
 #include "components/password_manager/core/browser/password_form.h"
 #include "components/password_manager/core/browser/password_manager_test_utils.h"
 #include "components/password_manager/core/browser/password_store/password_form_converters.h"
@@ -56,6 +57,10 @@
               GetAccountPasswordStore,
               (),
               (const, override));
+  MOCK_METHOD(bool,
+              IsReauthBeforeFillingRequired,
+              (device_reauth::DeviceAuthenticator * authenticator),
+              (override));
   MOCK_METHOD(PrefService*, GetPrefs, (), (const, override));
 };
 
@@ -327,9 +332,7 @@
 
 TEST_F(CredentialManagerPendingRequestTaskTest,
        SilentRequestFailsIfBiometricReauthEnabled) {
-  ON_CALL(*client()->GetPasswordFeatureManager(),
-          IsBiometricAuthenticationBeforeFillingEnabled)
-      .WillByDefault(Return(true));
+  ON_CALL(*client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true));
 
   form_.in_store = PasswordForm::Store::kProfileStore;
   profile_store_->AddLogin(password_manager::FromPasswordForm(form_));
@@ -352,9 +355,7 @@
 
 TEST_F(CredentialManagerPendingRequestTaskTest,
        NoAutosigninIfBiometricReauthEnabled) {
-  ON_CALL(*client()->GetPasswordFeatureManager(),
-          IsBiometricAuthenticationBeforeFillingEnabled)
-      .WillByDefault(Return(true));
+  ON_CALL(*client(), IsReauthBeforeFillingRequired).WillByDefault(Return(true));
 
   profile_store_->AddLogin(password_manager::FromPasswordForm(form_));
   RunAllPendingTasks();
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential credential re-auth bypass via Credential Management API on Android

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: A potential security bypass in Chrome for Android allows page JavaScript to retrieve plaintext credentials via the Credential Management API without triggering the user’s screen-lock or biometric re-authentication setting. This occurs because the API relies on a desktop-only feature query rather than the platform-appropriate Android re-authentication check. As a result, passwords can potentially be accessed without the configured user-security prompt.

Affected files:

  • components/password_manager/core/browser/credential_manager_pending_request_task.cc
  • components/password_manager/core/browser/password_feature_manager_impl.cc
  • chrome/browser/password_manager/chrome_password_manager_client.cc

Estimated timestamp from git blame: 2026-04-20

Summary

There is a potential security bypass on Chrome for Android where page JavaScript (or an XSS payload) on a website can retrieve the user’s plaintext saved password via navigator.credentials.get() without triggering the user’s configured biometric or screen lock (LSKF) re-authentication setting (i.e. “Use screen lock when filling passwords”).

Root Cause Analysis

In CredentialManagerPendingRequestTask::ProcessForms() (components/password_manager/core/browser/credential_manager_pending_request_task.cc at line 199), the auto-signin and silent mediation paths verify if biometric reauth is enabled before proceeding:

const bool is_biometric_reauth_enabled =
    delegate_->client()
        ->GetPasswordFeatureManager()
        ->IsBiometricAuthenticationBeforeFillingEnabled();

Similarly, in the manual selection / account chooser path, CredentialManagerImpl::SendPasswordForm() (components/password_manager/core/browser/credential_manager_impl.cc at line 266) also gates reauth checks on this method:

if (client_->GetPasswordFeatureManager()
        ->IsBiometricAuthenticationBeforeFillingEnabled()) {

However, PasswordFeatureManagerImpl::IsBiometricAuthenticationBeforeFillingEnabled() in components/password_manager/core/browser/password_feature_manager_impl.cc (line 38) is configured to return false on Android:

bool PasswordFeatureManagerImpl::IsBiometricAuthenticationBeforeFillingEnabled() const {
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
  ...
#else
  return false;
#endif
}

On Android, the correct platform-abstracted mechanism to check if re-authentication is required is PasswordManagerClient::IsReauthBeforeFillingRequired() (defined in chrome/browser/password_manager/chrome_password_manager_client.cc at line 703, which is used by standard password-filling interfaces such as Touch-to-Fill and the Keyboard Accessory).

Because the Credential Management API queries the desktop-only IsBiometricAuthenticationBeforeFillingEnabled(), is_biometric_reauth_enabled evaluates to false on Android. This allows the browser to perform silent auto-signins and return the plaintext password to the webpage’s JavaScript without presenting any biometric or screen-lock (LSKF) prompt to the user.

Potential Attack Steps

An attacker could potentially trigger the vulnerability with the following steps:

  1. Ensure the user has “Use screen lock when filling passwords” enabled in Chrome Settings -> Password Manager, and a screen lock is set up.
  2. Ensure the user has a saved credential for https://victim.example with skip_zero_click set to false.
  3. Through an XSS payload or on a page controlled by the attacker on https://victim.example, execute:
    navigator.credentials.get({password: true, mediation: 'silent'})
      .then(credential => {
        if (credential) {
          // Potential retrieval of the plaintext password without user re-auth
          alert(credential.password);
        }
      });
    
  4. Since the reauth check returns false, Chrome would retrieve the password and return it to the page context without presenting a biometric/LSKF prompt.

Note: These are suggested and potential steps; our security review is based on static analysis of the codebase, and our tooling does not currently have the capability to execute code to run a functional PoC.

Suggested Fix

To resolve this issue, the Credential Management API checks should use a platform-abstracted re-authentication check. Specifically, the reauthentication check in CredentialManagerPendingRequestTask and CredentialManagerImpl should be updated to query PasswordManagerClient::IsReauthBeforeFillingRequired() (or a platform-abstracted equivalent) to correctly honor Android’s biometric and screen lock re-authentication settings.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


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