CVE-2026-17825
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/password_manager/android/account_chooser_dialog_android.cc |
modified | |
MockPasswordManagerClientchrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc |
modified | |
AccountChooserDialogAndroidTestchrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc |
modified | |
TEST_Fchrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc |
modified |
Files Changed
chrome/browser/password_manager/android/account_chooser_dialog_android.ccchrome/browser/password_manager/android/account_chooser_dialog_android.hchrome/browser/password_manager/android/account_chooser_dialog_android_unittest.cc
Patch
From 323927c81c1702c6d111450afd87c5faf1f795af Mon Sep 17 00:00:00 2001 From: Anna Tsvirchkova <[email protected]> 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 <[email protected]> Commit-Queue: Anna Tsvirchkova <[email protected]> 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(); - } -
Regression Test / PoC
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();
Original Bug Report
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.cccomponents/password_manager/core/browser/password_feature_manager_impl.ccchrome/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:
- Ensure the user has “Use screen lock when filling passwords” enabled in Chrome Settings -> Password Manager, and a screen lock is set up.
- Ensure the user has a saved credential for
https://victim.examplewithskip_zero_clickset tofalse. - 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); } }); - 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.