CVE-2026-11193
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/password_manager/core/browser/credential_manager_impl.cc |
modified | |
TEST_Pcomponents/password_manager/core/browser/credential_manager_impl_unittest.cc |
modified |
Files Changed
components/password_manager/core/browser/credential_manager_impl.cccomponents/password_manager/core/browser/credential_manager_impl.hcomponents/password_manager/core/browser/credential_manager_impl_unittest.cccomponents/password_manager/core/browser/credential_manager_pending_request_task.cc
Patch
From 6b43fb95af8d28307019216e69f50db315ebf4fe Mon Sep 17 00:00:00 2001
From: Vasilii Sukhanov <vasilii@chromium.org>
Date: Mon, 20 Apr 2026 05:37:07 -0700
Subject: [PATCH] Respect biometric reauth setting in the Credential Management API.
Fixed: 503642586
Change-Id: I4dd706f98bfbdecf94b3bc87721362d48a6195f3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7770169
Reviewed-by: Viktor Semeniuk <vsemeniuk@google.com>
Commit-Queue: Vasilii Sukhanov <vasilii@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1617433}
---
diff --git a/components/password_manager/core/browser/credential_manager_impl.cc b/components/password_manager/core/browser/credential_manager_impl.cc
index 29fdd7f..11e6b7d7 100644
--- a/components/password_manager/core/browser/credential_manager_impl.cc
+++ b/components/password_manager/core/browser/credential_manager_impl.cc
@@ -10,8 +10,11 @@
#include "base/feature_list.h"
#include "base/functional/bind.h"
+#include "base/functional/callback_helpers.h"
#include "base/metrics/user_metrics.h"
+#include "base/strings/utf_string_conversions.h"
#include "components/affiliations/core/browser/affiliation_utils.h"
+#include "components/device_reauth/device_authenticator.h"
#include "components/password_manager/core/browser/credential_manager_logger.h"
#include "components/password_manager/core/browser/credential_manager_pending_request_task.h"
#include "components/password_manager/core/browser/credential_manager_utils.h"
@@ -19,11 +22,15 @@
#include "components/password_manager/core/browser/form_fetcher_impl.h"
#include "components/password_manager/core/browser/form_saver.h"
#include "components/password_manager/core/browser/leak_detection/leak_detection_request_utils.h"
+#include "components/password_manager/core/browser/password_feature_manager.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_manager_interface.h"
#include "components/password_manager/core/browser/password_manager_util.h"
+#include "components/password_manager/core/browser/password_ui_utils.h"
#include "components/password_manager/core/common/credential_manager_types.h"
#include "components/password_manager/core/common/password_manager_pref_names.h"
+#include "components/strings/grit/components_strings.h"
+#include "ui/base/l10n/l10n_util.h"
#include "url/origin.h"
namespace password_manager {
@@ -37,6 +44,17 @@
std::move(callback).Run(CredentialManagerError::SUCCESS, info);
}
+void OnReauthComplete(
+ base::OnceCallback<void(const CredentialInfo&)> send_cred_callback,
+ CredentialInfo info,
+ bool auth_succeeded) {
+ if (!auth_succeeded) {
+ std::move(send_cred_callback).Run(CredentialInfo());
+ return;
+ }
+ std::move(send_cred_callback).Run(info);
+}
+
} // namespace
CredentialManagerImpl::CredentialManagerImpl(PasswordManagerClient* client)
@@ -244,6 +262,37 @@
base::UserMetricsAction("CredentialManager_AccountChooser_Accepted"));
metrics_util::LogCredentialManagerGetResult(
metrics_util::CredentialManagerGetResult::kAccountChooser, mediation);
+
+ if (client_->GetPasswordFeatureManager()
+ ->IsBiometricAuthenticationBeforeFillingEnabled()) {
+ auto authenticator = client_->GetDeviceAuthenticator();
+ if (authenticator) {
+ std::u16string message;
+#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS)
+ const std::u16string origin =
+ base::UTF8ToUTF16(password_manager::GetShownOrigin(
+ client_->GetLastCommittedOrigin()));
+ message = l10n_util::GetStringFUTF16(
+ IDS_PASSWORD_MANAGER_FILLING_REAUTH, origin);
+#endif
+ auto send_cred_callback = base::BindOnce(
+ &CredentialManagerImpl::SendCredential,
+ weak_ptr_factory_.GetWeakPtr(), std::move(send_callback));
+
+ auto* authenticator_ptr = authenticator.get();
+ base::OnceClosure cleanup =
+ base::DoNothingWithBoundArgs(std::move(authenticator));
+
+ authenticator_ptr->AuthenticateWithMessage(
+ message,
+ metrics_util::TimeCallbackMediumTimes(
+ base::BindOnce(&OnReauthComplete, std::move(send_cred_callback),
+ info)
+ .Then(std::move(cleanup)),
+ "PasswordManager.PasswordFilling.AuthenticationTime2"));
+ return;
+ }
+ }
} else {
base::RecordAction(
base::UserMetricsAction("CredentialManager_AccountChooser_Dismissed"));
diff --git a/components/password_manager/core/browser/credential_manager_impl.h b/components/password_manager/core/browser/credential_manager_impl.h
index a69e53b3..be96b64 100644
--- a/components/password_manager/core/browser/credential_manager_impl.h
+++ b/components/password_manager/core/browser/credential_manager_impl.h
@@ -9,6 +9,7 @@
#include <vector>
#include "base/memory/raw_ptr.h"
+#include "base/memory/weak_ptr.h"
#include "build/build_config.h"
#include "components/credential_management/credential_manager_interface.h"
#include "components/password_manager/core/browser/credential_manager_password_form_manager.h"
@@ -105,6 +106,8 @@
// `Store` (if it was available) and reset in `OnProvisionalSaveComplete`.
// Only used on desktop.
std::optional<PasswordForm> last_submitted_form_;
+
+ base::WeakPtrFactory<CredentialManagerImpl> weak_ptr_factory_{this};
};
} // namespace password_manager
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 61f928a4..507f68d0 100644
--- a/components/password_manager/core/browser/credential_manager_impl_unittest.cc
+++ b/components/password_manager/core/browser/credential_manager_impl_unittest.cc
@@ -24,6 +24,7 @@
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/affiliations/core/browser/fake_affiliation_service.h"
+#include "components/device_reauth/mock_device_authenticator.h"
#include "components/password_manager/core/browser/affiliation/mock_affiliated_match_helper.h"
#include "components/password_manager/core/browser/credential_manager_pending_request_task.h"
#include "components/password_manager/core/browser/credential_manager_utils.h"
@@ -32,6 +33,7 @@
#include "components/password_manager/core/browser/leak_detection/leak_detection_check_factory.h"
#include "components/password_manager/core/browser/leak_detection/leak_detection_request_utils.h"
#include "components/password_manager/core/browser/leak_detection/mock_leak_detection_check_factory.h"
+#include "components/password_manager/core/browser/password_feature_manager.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_form_manager_for_ui.h"
#include "components/password_manager/core/browser/password_manager.h"
@@ -108,6 +110,10 @@
(base::span<const PasswordForm>),
bool was_autofilled_on_pageload),
(override));
+ MOCK_METHOD(std::unique_ptr<device_reauth::DeviceAuthenticator>,
+ GetDeviceAuthenticator,
+ (),
+ (override));
#if !BUILDFLAG(IS_ANDROID)
MOCK_METHOD(bool, IsActorTaskActive, (), (override));
#endif
@@ -2022,4 +2028,42 @@
testing::Bool());
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+TEST_P(CredentialManagerImplTest, ReauthAfterAccountSelection) {
+ ON_CALL(*client_->GetPasswordFeatureManager(),
+ IsBiometricAuthenticationBeforeFillingEnabled)
+ .WillByDefault(Return(true));
+
+ store_->AddLogin(form_);
+ RunAllPendingTasks();
+
+ EXPECT_CALL(*client_, PromptUserToChooseCredentialsPtr).Times(1);
+
+ auto mock_authenticator =
+ std::make_unique<device_reauth::MockDeviceAuthenticator>();
+ device_reauth::MockDeviceAuthenticator* raw_authenticator =
+ mock_authenticator.get();
+ EXPECT_CALL(*client_, GetDeviceAuthenticator)
+ .WillOnce(Return(std::move(mock_authenticator)));
+
+ EXPECT_CALL(*raw_authenticator, AuthenticateWithMessage)
+ .WillOnce([](const std::u16string& message,
+ device_reauth::MockDeviceAuthenticator::AuthenticateCallback
+ callback) {
+ std::move(callback).Run(true); // Success
+ });
+
+ bool called = false;
+ CredentialManagerError error;
+ std::optional<CredentialInfo> credential;
+ CallGet(CredentialMediationRequirement::kOptional, /*include_passwords=*/true,
+ /*federations=*/{},
+ base::BindOnce(&GetCredentialCallback, &called, &error, &credential));
+
+ RunAllPendingTasks();
+
+ EXPECT_TRUE(called);
+ EXPECT_EQ(CredentialManagerError::SUCCESS, error);
+ EXPECT_EQ(form_.username_value, credential->id);
+}
+
} // namespace password_manager
diff --git a/components/password_manager/core/browser/credential_manager_pending_request_task.cc b/components/password_manager/core/browser/credential_manager_pending_request_task.cc
index cdb48f42..815eabbc 100644
--- a/components/password_manager/core/browser/credential_manager_pending_request_task.cc
+++ b/components/password_manager/core/browser/credential_manager_pending_request_task.cc
Regression Test / PoC
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 61f928a4..507f68d0 100644
--- a/components/password_manager/core/browser/credential_manager_impl_unittest.cc
+++ b/components/password_manager/core/browser/credential_manager_impl_unittest.cc
@@ -24,6 +24,7 @@
#include "base/test/task_environment.h"
#include "build/build_config.h"
#include "components/affiliations/core/browser/fake_affiliation_service.h"
+#include "components/device_reauth/mock_device_authenticator.h"
#include "components/password_manager/core/browser/affiliation/mock_affiliated_match_helper.h"
#include "components/password_manager/core/browser/credential_manager_pending_request_task.h"
#include "components/password_manager/core/browser/credential_manager_utils.h"
@@ -32,6 +33,7 @@
#include "components/password_manager/core/browser/leak_detection/leak_detection_check_factory.h"
#include "components/password_manager/core/browser/leak_detection/leak_detection_request_utils.h"
#include "components/password_manager/core/browser/leak_detection/mock_leak_detection_check_factory.h"
+#include "components/password_manager/core/browser/password_feature_manager.h"
#include "components/password_manager/core/browser/password_form.h"
#include "components/password_manager/core/browser/password_form_manager_for_ui.h"
#include "components/password_manager/core/browser/password_manager.h"
@@ -108,6 +110,10 @@
(base::span<const PasswordForm>),
bool was_autofilled_on_pageload),
(override));
+ MOCK_METHOD(std::unique_ptr<device_reauth::DeviceAuthenticator>,
+ GetDeviceAuthenticator,
+ (),
+ (override));
#if !BUILDFLAG(IS_ANDROID)
MOCK_METHOD(bool, IsActorTaskActive, (), (override));
#endif
@@ -2022,4 +2028,42 @@
testing::Bool());
#endif // !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
+TEST_P(CredentialManagerImplTest, ReauthAfterAccountSelection) {
+ ON_CALL(*client_->GetPasswordFeatureManager(),
+ IsBiometricAuthenticationBeforeFillingEnabled)
+ .WillByDefault(Return(true));
+
+ store_->AddLogin(form_);
+ RunAllPendingTasks();
+
+ EXPECT_CALL(*client_, PromptUserToChooseCredentialsPtr).Times(1);
+
+ auto mock_authenticator =
+ std::make_unique<device_reauth::MockDeviceAuthenticator>();
+ device_reauth::MockDeviceAuthenticator* raw_authenticator =
+ mock_authenticator.get();
+ EXPECT_CALL(*client_, GetDeviceAuthenticator)
+ .WillOnce(Return(std::move(mock_authenticator)));
+
+ EXPECT_CALL(*raw_authenticator, AuthenticateWithMessage)
+ .WillOnce([](const std::u16string& message,
+ device_reauth::MockDeviceAuthenticator::AuthenticateCallback
+ callback) {
+ std::move(callback).Run(true); // Success
+ });
+
+ bool called = false;
+ CredentialManagerError error;
+ std::optional<CredentialInfo> credential;
+ CallGet(CredentialMediationRequirement::kOptional, /*include_passwords=*/true,
+ /*federations=*/{},
+ base::BindOnce(&GetCredentialCallback, &called, &error, &credential));
+
+ RunAllPendingTasks();
+
+ EXPECT_TRUE(called);
+ EXPECT_EQ(CredentialManagerError::SUCCESS, error);
+ EXPECT_EQ(form_.username_value, credential->id);
+}
+
} // 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 1bbddc9..c136d097 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
@@ -324,4 +324,55 @@
RunAllPendingTasks();
}
+TEST_F(CredentialManagerPendingRequestTaskTest,
+ SilentRequestFailsIfBiometricReauthEnabled) {
+ ON_CALL(*client()->GetPasswordFeatureManager(),
+ IsBiometricAuthenticationBeforeFillingEnabled)
+ .WillByDefault(Return(true));
+
+ form_.in_store = PasswordForm::Store::kProfileStore;
+ profile_store_->AddLogin(form_);
+ RunAllPendingTasks();
+
+ EXPECT_CALL(*client(), NotifyUserAutoSignin).Times(0);
+ EXPECT_CALL(*client(), PromptUserToChooseCredentials).Times(0);
+
+ EXPECT_CALL(
+ delegate_mock_,
+ SendCredential(_, testing::Field(&CredentialInfo::type,
+ CredentialType::CREDENTIAL_TYPE_EMPTY)));
+
+ CredentialManagerPendingRequestTask task(
+ &delegate_mock_, /*callback=*/base::DoNothing(),
+ CredentialMediationRequirement::kSilent, /*include_passwords=*/true,
+ /*request_federations=*/{}, GetFormDigest());
+ RunAllPendingTasks();
+}
+
+TEST_F(CredentialManagerPendingRequestTaskTest,
+ NoAutosigninIfBiometricReauthEnabled) {
+ ON_CALL(*client()->GetPasswordFeatureManager(),
+ IsBiometricAuthenticationBeforeFillingEnabled)
+ .WillByDefault(Return(true));
+
+ profile_store_->AddLogin(form_);
+ RunAllPendingTasks();
+
+ std::vector<std::unique_ptr<PasswordForm>> expected_forms;
+ form_.in_store = PasswordForm::Store::kProfileStore;
+ expected_forms.push_back(std::make_unique<PasswordForm>(form_));
+
+ EXPECT_CALL(*client(), NotifyUserAutoSignin).Times(0);
+ EXPECT_CALL(*client(),
+ PromptUserToChooseCredentials(
+ UnorderedPasswordFormElementsAre(&expected_forms), _, _));
+ EXPECT_CALL(delegate_mock_, IsZeroClickAllowed).Times(0);
+
+ CredentialManagerPendingRequestTask task(
+ &delegate_mock_, /*callback=*/base::DoNothing(),
+ CredentialMediationRequirement::kOptional, /*include_passwords=*/true,
+ /*request_federations=*/{}, GetFormDigest());
+ RunAllPendingTasks();
+}
+
} // namespace password_manager
Original Bug Report
Potential bypass of biometric re-authentication for passwords via Credential Management API
Flapjack, 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 without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.
Overview: The “Use your screen lock when filling passwords” security feature can be bypassed when a site uses the Credential Management API to trigger an auto sign-in. The underlying C++ task fails to check if re-authentication is required before sending the plaintext password back to the renderer, allowing silent extraction of credentials without a biometric prompt.
Affected files:
components/password_manager/core/browser/credential_manager_pending_request_task.cccomponents/password_manager/core/browser/credential_manager_impl.cc
Estimated timestamp from git blame: 2023-08-04
Description
When a user enables “Use your screen lock when filling passwords” (kBiometricAuthenticationBeforeFilling), Chrome should require OS-level biometric or device lock authentication before filling or exposing saved passwords. This feature protects against local attackers extracting passwords from an unlocked device.
However, a logic flaw exists in the Credential Management API’s auto sign-in flow. By default, Chrome enables “Sign in automatically” (kCredentialsEnableAutosignin). If a user has exactly one saved password for a site, and hasn’t explicitly opted out of auto sign-in for that site (which sets skip_zero_click), the API allows silent retrieval of the credential.
When navigator.credentials.get({password: true, mediation: 'silent'}) is called, the browser process evaluates whether to perform an auto sign-in. Because the auto sign-in logic completely omits the IsReauthBeforeFillingRequired() check, the browser returns the plaintext password to the web page immediately, bypassing the user-enabled biometric protection.
Vulnerable Code Analysis
When the renderer requests a password, the browser delegates the fetch to CredentialManagerPendingRequestTask. Once the PasswordStore returns the credentials, the task processes them in ProcessForms() (components/password_manager/core/browser/credential_manager_pending_request_task.cc).
The code evaluates can_use_autosignin based on mediation requirements, the number of results, and the zero-click preference. If auto sign-in is allowed, it executes the following block:
if (can_use_autosignin && !results[0]->skip_zero_click &&
!password_bubble_experiment::ShouldShowAutoSignInPromptFirstRunExperience(
delegate_->client()->GetPrefs())) {
auto info = PasswordFormToCredentialInfo(*results[0]);
delegate_->client()->NotifyUserAutoSignin(std::move(results), origin_);
base::RecordAction(base::UserMetricsAction("CredentialManager_Autosignin"));
LogCredentialManagerGetResult(
metrics_util::CredentialManagerGetResult::kAutoSignIn, mediation_);
delegate_->SendCredential(std::move(send_callback_), info);
return;
}
The code directly populates the CredentialInfo struct with the plaintext password from the PasswordForm and sends it back to the renderer via delegate_->SendCredential.
Unlike other password filling paths (such as PasswordAutofillManager::OnPasswordCredentialSuggestionAccepted or AccountChooserDialogAndroid::HandleCredentialChosen), this specific path never calls client_->IsReauthBeforeFillingRequired() or triggers the OS-level authenticator (authenticator_->AuthenticateWithMessage).
Suggested Steps to Reproduce
Note: These are potential steps as our tooling agent does not have the ability to run code directly.
- In Chrome, save exactly one password for a test website (e.g.,
https://example.com). - Go to
chrome://password-manager/settingsand enable “Use your screen lock when filling passwords”. - Ensure “Sign in automatically” is enabled (this is the default setting).
- Navigate to the test website.
- Open Developer Tools and execute the following in the console:
let cred = await navigator.credentials.get({password: true, mediation: 'silent'}); console.log(cred.password); - Observe that the plaintext password is printed to the console immediately, without any OS biometric or screen lock prompt appearing.
Impact
An attacker with physical access to an unlocked device, or a malicious script running on a site via XSS, can silently extract the user’s plaintext password. This defeats the security guarantee provided by the biometric authentication feature.
Suggested Fix
In CredentialManagerPendingRequestTask::ProcessForms, before calling delegate_->SendCredential in the auto sign-in path, add a check for delegate_->client()->IsReauthBeforeFillingRequired(). If re-authentication is required, the task should pause and trigger the OS-level authenticator. Only upon successful authentication should the callback resolve with the CredentialInfo. If authentication fails or is canceled, it should return an empty credential or an error, similar to the logic in AccountChooserDialogAndroid.
Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f
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.