CVE-2026-17829
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
backup_passwordcomponents/autofill/core/browser/suggestions/suggestion.cc |
modified | |
is_cross_domaincomponents/autofill/core/browser/suggestions/suggestion.cc |
modified | |
ifcomponents/password_manager/core/browser/password_autofill_manager.cc |
modified | |
TEST_Fcomponents/password_manager/core/browser/password_autofill_manager_unittest.cc |
modified |
Files Changed
components/autofill/core/browser/suggestions/suggestion.cccomponents/autofill/core/browser/suggestions/suggestion.hcomponents/password_manager/core/browser/password_autofill_manager.cccomponents/password_manager/core/browser/password_autofill_manager_unittest.cccomponents/password_manager/core/browser/password_suggestion_generator.cc
Patch
From aae1302514385edd589e5cdc051d17bce2f3a802 Mon Sep 17 00:00:00 2001 From: Oleksandr Tara <[email protected]> Date: Mon, 08 Jun 2026 08:47:20 -0700 Subject: [PATCH] Show cross-domain confirmation prior to filling backup passwords. - Re-enable proactive recovery for grouped matches by removing the match type filter. - Support gating grouped backup credential filling behind the cross-domain confirmation popup. - Use `signon_realm` from Suggestion::PasswordSuggestionDetails to track grouped matches for backup passwords. - Update the Suggestion::PasswordSuggestionDetails backup password constructor to accept signon_realm directly, updating all production and test call sites. Fixed: b:517705103 Change-Id: I9fdc77ec32df5082290d2bb1323b70535aa1344d Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7894627 Reviewed-by: Viktor Semeniuk <[email protected]> Commit-Queue: Oleksandr Tara <[email protected]> Reviewed-by: Jihad Hanna <[email protected]> Reviewed-by: Timofey Chudakov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1643235} --- diff --git a/components/autofill/core/browser/suggestions/suggestion.cc b/components/autofill/core/browser/suggestions/suggestion.cc index 2e3a95b39..8253d00c 100644 --- a/components/autofill/core/browser/suggestions/suggestion.cc +++ b/components/autofill/core/browser/suggestions/suggestion.cc @@ -206,10 +206,14 @@ Suggestion::PasswordSuggestionDetails::PasswordSuggestionDetails( std::u16string_view username, std::u16string_view password, - std::u16string_view backup_password) + std::u16string_view backup_password, + std::string_view signon_realm, + bool is_cross_domain) : username(username), password(password), - backup_password(backup_password) {} + backup_password(backup_password), + signon_realm(signon_realm), + is_cross_domain(is_cross_domain) {} Suggestion::PasswordSuggestionDetails::PasswordSuggestionDetails( const PasswordSuggestionDetails&) = default; diff --git a/components/autofill/core/browser/suggestions/suggestion.h b/components/autofill/core/browser/suggestions/suggestion.h index 7f51bca..b6f37e1d 100644 --- a/components/autofill/core/browser/suggestions/suggestion.h +++ b/components/autofill/core/browser/suggestions/suggestion.h @@ -62,11 +62,13 @@ // Stores either the password signon realm or the Android app name for which // the password was saved. std::optional<std::u16string> display_signon_realm; - // This flag is set to `false` for the manual fallback suggestions which - // represent exact, strongly affiliated, PSL and weakly affiliated matches - // for the domain the suggestions are shown for. All other manual fallback - // suggestions have this flag set to `true`. - // Note that non-manual-fallback suggestions are never cross domain. + // Indicates if the suggestions represents a credential for which we are + // unsure if it belongs to the current website, and thus should show a + // confirmation popup when filling the credential. For manual fallback + // suggestions, this is set to `false` if they represent exact, strongly + // affiliated or PSL matches, and `true` for all others (such as + // grouped/weakly affiliated matches). For backup suggestions, this is set + // to `true` if they represent a grouped match. bool is_cross_domain = false; PasswordSuggestionDetails(); @@ -78,7 +80,9 @@ // Used to construct the payload of a backup password suggestion. PasswordSuggestionDetails(std::u16string_view username, std::u16string_view password, - std::u16string_view backup_password); + std::u16string_view backup_password, + std::string_view signon_realm, + bool is_cross_domain); PasswordSuggestionDetails(const PasswordSuggestionDetails&); PasswordSuggestionDetails(PasswordSuggestionDetails&&); PasswordSuggestionDetails& operator=(const PasswordSuggestionDetails&); diff --git a/components/password_manager/core/browser/password_autofill_manager.cc b/components/password_manager/core/browser/password_autofill_manager.cc index fe53f57..4c3b661 100644 --- a/components/password_manager/core/browser/password_autofill_manager.cc +++ b/components/password_manager/core/browser/password_autofill_manager.cc @@ -374,9 +374,28 @@ auto payload = suggestion .GetPayload<autofill::Suggestion::PasswordSuggestionDetails>(); - OnPasswordCredentialSuggestionAccepted( + auto fill_backup_callback = base::BindOnce(&PasswordAutofillManager::FillBackupSuggestion, - weak_ptr_factory_.GetWeakPtr(), payload)); + weak_ptr_factory_.GetWeakPtr(), payload); + if (payload.is_cross_domain) { +#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN) || BUILDFLAG(IS_CHROMEOS) || \ + BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_ANDROID) + cross_domain_confirmation_controller_ = + password_client_->ShowCrossDomainConfirmationPopup( + last_popup_open_args_.element_bounds, + last_popup_open_args_.text_direction, + password_manager_driver_->GetLastCommittedURL(), + password_manager_util::GetHumanReadableRealm( + payload.signon_realm.value_or(std::string())), + /*show_warning_text=*/true, + base::BindOnce(&PasswordAutofillManager:: + OnPasswordCredentialSuggestionAccepted, + weak_ptr_factory_.GetWeakPtr(), + std::move(fill_backup_callback))); +#endif + } else { + OnPasswordCredentialSuggestionAccepted(std::move(fill_backup_callback)); + } break; } case autofill::SuggestionType::kTroubleSigningInEntry: { @@ -929,10 +948,12 @@ if (proactive_recovery_login) { CHECK(proactive_recovery_login->backup_password_value); - const auto suggestion_details = Suggestion::PasswordSuggestionDetails( + auto suggestion_details = Suggestion::PasswordSuggestionDetails( proactive_recovery_login->username_value, proactive_recovery_login->password_value, - proactive_recovery_login->backup_password_value.value()); + proactive_recovery_login->backup_password_value.value(), + proactive_recovery_login->realm, + proactive_recovery_login->is_grouped_affiliation); return suggestion_generator_.GetProactiveRecoverySuggestions( suggestion_details); } diff --git a/components/password_manager/core/browser/password_autofill_manager_unittest.cc b/components/password_manager/core/browser/password_autofill_manager_unittest.cc index 270d1342..8d0dfd92 100644 --- a/components/password_manager/core/browser/password_autofill_manager_unittest.cc +++ b/components/password_manager/core/browser/password_autofill_manager_unittest.cc @@ -2144,6 +2144,26 @@ "PasswordManager.FillSuggestionsGroupedMatchAccepted", /*sample=*/false, /*expected_bucket_count=*/1); } + +TEST_F(PasswordAutofillManagerTest, + PasswordRecoveryFlow_GroupedBackupSuggestion_TriggersConfirmation) { + fill_data().preferred_login.backup_password_value = kAliceBackupPassword; + TestPasswordManagerClient client; + NiceMock<MockAutofillClient> autofill_client; + InitializePasswordAutofillManager(&client, &autofill_client); + Suggestion::PasswordSuggestionDetails payload( + test_username_, test_password_, backup_password_, + /*signon_realm=*/"https://grouped.com/", /*is_cross_domain=*/true); + + EXPECT_CALL(client, ShowCrossDomainConfirmationPopup); + EXPECT_CALL(*client.mock_driver(), FillSuggestion).Times(0); + + password_autofill_manager_->DidAcceptSuggestion( + autofill::test::CreateAutofillSuggestion( + autofill::SuggestionType::kBackupPasswordEntry, test_username_, + payload), + SuggestionPosition{.row = 0}); +} #endif TEST_F(PasswordAutofillManagerTest, WaitForPasskeysWithAutofocusTrigger) { @@ -2288,7 +2308,8 @@ NiceMock<MockAutofillClient> autofill_client; InitializePasswordAutofillManager(&client, &autofill_client); const Suggestion::Payload& payload = Suggestion::PasswordSuggestionDetails( - test_username_, test_password_, backup_password_); + test_username_, test_password_, backup_password_, + /*signon_realm=*/"", /*is_cross_domain=*/false); EXPECT_CALL(*client.mock_driver(), FillSuggestion(test_username_, backup_password_, _)); @@ -2311,7 +2332,8 @@ TestPasswordManagerClient client; InitializePasswordAutofillManager(&client, nullptr); const Suggestion::PasswordSuggestionDetails payload( - test_username_, test_password_, backup_password_); + test_username_, test_password_, backup_password_, + /*signon_realm=*/"", /*is_cross_domain=*/false); const Suggestion suggestion = autofill::test::CreateAutofillSuggestion( autofill::SuggestionType::kBackupPasswordEntry, test_username_, payload); @@ -2336,7 +2358,8 @@ InitializePasswordAutofillManager(&client, nullptr); const Suggestion::PasswordSuggestionDetails payload( - test_username_, test_password_, backup_password_); + test_username_, test_password_, backup_password_, + /*signon_realm=*/"", /*is_cross_domain=*/false); const Suggestion suggestion = autofill::test::CreateAutofillSuggestion( autofill::SuggestionType::kBackupPasswordEntry, test_username_, payload); @@ -2352,7 +2375,8 @@ NiceMock<MockAutofillClient> autofill_client; InitializePasswordAutofillManager(&client, &autofill_client); const Suggestion::Payload& payload = Suggestion::PasswordSuggestionDetails( - test_username_, test_password_, backup_password_); + test_username_, test_password_, backup_password_, + /*signon_realm=*/"", /*is_cross_domain=*/false); EXPECT_CALL(*client.mock_driver(), FillSuggestion(test_username_, backup_password_, _)); diff --git a/components/password_manager/core/browser/password_suggestion_generator.cc b/components/password_manager/core/browser/password_suggestion_generator.cc index b131e4d0..910dd341 100644 --- a/components/password_manager/core/browser/password_suggestion_generator.cc +++ b/components/password_manager/core/browser/password_suggestion_generator.cc
Regression Test / PoC
diff --git a/components/password_manager/core/browser/password_autofill_manager_unittest.cc b/components/password_manager/core/browser/password_autofill_manager_unittest.cc
index 270d1342..8d0dfd92 100644
--- a/components/password_manager/core/browser/password_autofill_manager_unittest.cc
+++ b/components/password_manager/core/browser/password_autofill_manager_unittest.cc
@@ -2144,6 +2144,26 @@
"PasswordManager.FillSuggestionsGroupedMatchAccepted", /*sample=*/false,
/*expected_bucket_count=*/1);
}
+
+TEST_F(PasswordAutofillManagerTest,
+ PasswordRecoveryFlow_GroupedBackupSuggestion_TriggersConfirmation) {
+ fill_data().preferred_login.backup_password_value = kAliceBackupPassword;
+ TestPasswordManagerClient client;
+ NiceMock<MockAutofillClient> autofill_client;
+ InitializePasswordAutofillManager(&client, &autofill_client);
+ Suggestion::PasswordSuggestionDetails payload(
+ test_username_, test_password_, backup_password_,
+ /*signon_realm=*/"https://grouped.com/", /*is_cross_domain=*/true);
+
+ EXPECT_CALL(client, ShowCrossDomainConfirmationPopup);
+ EXPECT_CALL(*client.mock_driver(), FillSuggestion).Times(0);
+
+ password_autofill_manager_->DidAcceptSuggestion(
+ autofill::test::CreateAutofillSuggestion(
+ autofill::SuggestionType::kBackupPasswordEntry, test_username_,
+ payload),
+ SuggestionPosition{.row = 0});
+}
#endif
TEST_F(PasswordAutofillManagerTest, WaitForPasskeysWithAutofocusTrigger) {
@@ -2288,7 +2308,8 @@
NiceMock<MockAutofillClient> autofill_client;
InitializePasswordAutofillManager(&client, &autofill_client);
const Suggestion::Payload& payload = Suggestion::PasswordSuggestionDetails(
- test_username_, test_password_, backup_password_);
+ test_username_, test_password_, backup_password_,
+ /*signon_realm=*/"", /*is_cross_domain=*/false);
EXPECT_CALL(*client.mock_driver(),
FillSuggestion(test_username_, backup_password_, _));
@@ -2311,7 +2332,8 @@
TestPasswordManagerClient client;
InitializePasswordAutofillManager(&client, nullptr);
const Suggestion::PasswordSuggestionDetails payload(
- test_username_, test_password_, backup_password_);
+ test_username_, test_password_, backup_password_,
+ /*signon_realm=*/"", /*is_cross_domain=*/false);
const Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
autofill::SuggestionType::kBackupPasswordEntry, test_username_, payload);
@@ -2336,7 +2358,8 @@
InitializePasswordAutofillManager(&client, nullptr);
const Suggestion::PasswordSuggestionDetails payload(
- test_username_, test_password_, backup_password_);
+ test_username_, test_password_, backup_password_,
+ /*signon_realm=*/"", /*is_cross_domain=*/false);
const Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
autofill::SuggestionType::kBackupPasswordEntry, test_username_, payload);
@@ -2352,7 +2375,8 @@
NiceMock<MockAutofillClient> autofill_client;
InitializePasswordAutofillManager(&client, &autofill_client);
const Suggestion::Payload& payload = Suggestion::PasswordSuggestionDetails(
- test_username_, test_password_, backup_password_);
+ test_username_, test_password_, backup_password_,
+ /*signon_realm=*/"", /*is_cross_domain=*/false);
EXPECT_CALL(*client.mock_driver(),
FillSuggestion(test_username_, backup_password_, _));
diff --git a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
index c02ed9a..d0a134d 100644
--- a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
@@ -239,7 +239,8 @@
const PasswordAndMetadata& credential) {
return Suggestion::PasswordSuggestionDetails(
credential.username_value, credential.password_value,
- credential.backup_password_value.value());
+ credential.backup_password_value.value(), credential.realm,
+ credential.is_grouped_affiliation);
}
class MockPasswordManagerClient : public StubPasswordManagerClient {
public:
diff --git a/components/password_manager/core/browser/undo_password_change_controller_unittest.cc b/components/password_manager/core/browser/undo_password_change_controller_unittest.cc
index 5b72771e..660f248 100644
--- a/components/password_manager/core/browser/undo_password_change_controller_unittest.cc
+++ b/components/password_manager/core/browser/undo_password_change_controller_unittest.cc
@@ -383,19 +383,23 @@
}
TEST_F(UndoPasswordChangeControllerTest,
- OnLoginPotentiallyFailed_GroupedAffiliation_Ignored) {
+ OnLoginPotentiallyFailed_GroupedAffiliation_RecoveryTriggered) {
best_match_form_.SetPasswordBackupNote(kBackupPassword);
best_match_form_.match_type =
password_manager::PasswordForm::MatchType::kGrouped;
auto form_manager = CreateFormManager(best_match_form_);
+ base::RunLoop run_loop;
controller_.OnLoginPotentiallyFailed(&driver_, failed_login_form_);
- EXPECT_CALL(driver_, TriggerPasswordRecoverySuggestions).Times(0);
+ EXPECT_CALL(driver_, TriggerPasswordRecoverySuggestions(
+ failed_login_form_.password_element_renderer_id))
+ .WillOnce(RunOnceClosure(run_loop.QuitClosure()));
static_cast<PasswordFormManagerObserver*>(&controller_)
->OnPasswordFormParsed(form_manager.get());
+ run_loop.Run();
EXPECT_EQ(controller_.GetState(kUsername),
- PasswordRecoveryState::kRegularFlow);
+ PasswordRecoveryState::kShowProactiveRecovery);
}
TEST_F(UndoPasswordChangeControllerTest,
Original Bug Report
Potential cross-domain credential fill bypass for backup passwords of grouped-affiliation sites
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 issue in Chrome’s password manager could allow a grouped-affiliation site to receive the backup password of another grouped site without displaying the required cross-domain confirmation popup. This occurs because the proactive password recovery state is keyed solely on username, enabling different credentials sharing the same username to inherit the recovery state. When the backup suggestion is accepted, the filling path fails to consult the cross-domain consent gate.
Affected files:
components/password_manager/core/browser/password_suggestion_generator.cccomponents/password_manager/core/browser/password_autofill_manager.cccomponents/password_manager/core/browser/undo_password_change_controller.cc
Estimated timestamp from git blame: 2025-07-03
Root Cause
There is a potential security boundary bypass in Chrome’s password manager regarding backup passwords for grouped-affiliation sites. While standard credential filling for grouped-affiliation domains requires an explicit cross-domain consent dialog, backup passwords (kBackupPasswordEntry) can bypass this protection.
First, in components/password_manager/core/browser/undo_password_change_controller.cc, IsFormEligibleForProactiveRecovery only blocks the credential that enters the recovery state if it is a grouped match:
bool IsFormEligibleForProactiveRecovery(
const StoredCredential* form_best_match, ...) {
...
if (password_manager_util::GetMatchType(*form_best_match) ==
password_manager_util::GetLoginMatchType::kGrouped) {
return false; // Only gates the ENTERING credential
}
...
}
If a user has both an exact-match credential and a grouped-affiliation credential with the same username, the exact match is used as the form_best_match. The eligibility check passes, and the recovery state transitions to kShowProactiveRecovery (and eventually kIncludeBackup upon dismissal). This state is keyed solely on the username via UndoPasswordChangeController::GetState(username).
Second, in components/password_manager/core/browser/password_suggestion_generator.cc, suggestions are generated for all matched credentials. If a grouped-match credential shares the username, it inherits the recovery state:
bool show_recovery_password =
undo_password_change_controller.GetState(credential.username_value) ==
PasswordRecoveryState::kIncludeBackup;
if (credential.backup_password_value && show_recovery_password) {
AppendBackupSuggestion(credential, suggestions);
}
AppendBackupSuggestion builds a suggestion of type SuggestionType::kBackupPasswordEntry but does not populate cross-domain details or realm tags in the payload.
Third, in components/password_manager/core/browser/password_autofill_manager.cc, the acceptance path for kBackupPasswordEntry lacks any cross-domain confirmation gate (unlike standard credentials which check is_grouped_affiliation and route through ShowCrossDomainConfirmationPopup):
case autofill::SuggestionType::kBackupPasswordEntry: {
...
auto payload =
suggestion
.GetPayload<autofill::Suggestion::PasswordSuggestionDetails>();
OnPasswordCredentialSuggestionAccepted(
base::BindOnce(&PasswordAutofillManager::FillBackupSuggestion,
weak_ptr_factory_.GetWeakPtr(), payload));
break;
}
As a result, selecting the grouped backup suggestion immediately fills the backup password into the current page without user warning.
Potential Attack Flow / Suggested Trigger Steps
Note: These are potential steps to trigger the behavior based on code review; our tooling agent does not currently have the capability to execute code.
- Ensure the
PasswordFormGroupedAffiliationsfeature is active. - In Chrome’s password store, save
[email protected]with a password forhttps://current.example(exact match) and[email protected]with a different password containing a backup password forhttps://grouped.example(grouped match). - Navigate to
https://current.example, enter[email protected]and an incorrect password, and submit the form. - Dismiss the proactive-recovery popup when it appears (setting state to
kIncludeBackup). - Focus the password field to open autofill suggestions. Observe that two identical suggestions for recovery passwords appear with no realm details to distinguish them.
- Select the second suggestion. The backup password for
grouped.exampleis successfully filled intocurrent.examplewithout displaying a cross-domain confirmation popup.
Suggested Fix
Ensure that backup suggestions (kBackupPasswordEntry) respect cross-domain boundaries:
- In
components/password_manager/core/browser/password_suggestion_generator.cc, when callingAppendBackupSuggestion, explicitly populate theis_cross_domainandsignon_realmproperties ofPasswordSuggestionDetailsif the credential is a grouped or cross-origin match. - In
components/password_manager/core/browser/password_autofill_manager.cc, interceptautofill::SuggestionType::kBackupPasswordEntryand ensure it is routed throughShowCrossDomainConfirmationPopupif the payload represents a cross-domain or grouped-affiliated credential.
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.