CVE-2026-13937
Overview
Files Changed
components/password_manager/core/browser/features/password_features.cccomponents/password_manager/core/browser/features/password_features.hcomponents/password_manager/core/browser/password_suggestion_generator.cccomponents/password_manager/core/browser/password_suggestion_generator_unittest.cc
Patch
From c28628eb69fbc173e011c2f1d7c238b1c2fa18d6 Mon Sep 17 00:00:00 2001
From: Timofey Chudakov <tchudakov@google.com>
Date: Fri, 22 May 2026 02:41:51 -0700
Subject: [PATCH] [PWM] Show confirmation dialog for grouped credentials.
All suggested passwords in the manual fallback popup were
considered safe to fill before this CL. Grouped (weakly affiliated)
passwords are now filled after an explicit user consent.
The functionality is hidden under a kill-switch to avoid
regressions.
Without the feature flag: http://shortn/_fIjfPOyHqu
With the feature flag: http://shortn/_G7SUlV4Ff9
Bug: 513046494
Change-Id: I231dac1067fe3bcc0d0fe7eedc39b039a6fd89f6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7859712
Commit-Queue: Timofey Chudakov <tchudakov@google.com>
Reviewed-by: Ioana Treib <ioanap@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1634827}
---
diff --git a/components/password_manager/core/browser/features/password_features.cc b/components/password_manager/core/browser/features/password_features.cc
index 7eef4477..f5f33a1 100644
--- a/components/password_manager/core/browser/features/password_features.cc
+++ b/components/password_manager/core/browser/features/password_features.cc
@@ -184,6 +184,11 @@
#endif
#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
+// Shows a confirmation dialog before filling grouped credentials from the
+// manual fallback popup on Desktop.
+BASE_FEATURE(kShowConfirmationForGroupedCredentials,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+
BASE_FEATURE(kShowTabWithPasswordChangeOnSuccess,
base::FEATURE_DISABLED_BY_DEFAULT);
diff --git a/components/password_manager/core/browser/features/password_features.h b/components/password_manager/core/browser/features/password_features.h
index 6c8b2917..9e1b72e 100644
--- a/components/password_manager/core/browser/features/password_features.h
+++ b/components/password_manager/core/browser/features/password_features.h
@@ -196,6 +196,10 @@
BASE_DECLARE_FEATURE(kRestartToGainAccessToKeychain);
#endif // BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
+// Shows a confirmation dialog before filling grouped credentials from the
+// manual fallback popup on Desktop.
+BASE_DECLARE_FEATURE(kShowConfirmationForGroupedCredentials);
+
// Shows a tab with password change instead of bubble/settings page after
// successful password change.
BASE_DECLARE_FEATURE(kShowTabWithPasswordChangeOnSuccess);
diff --git a/components/password_manager/core/browser/password_suggestion_generator.cc b/components/password_manager/core/browser/password_suggestion_generator.cc
index af7fa08..be2d000 100644
--- a/components/password_manager/core/browser/password_suggestion_generator.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator.cc
@@ -21,6 +21,7 @@
#include "components/autofill/core/browser/suggestions/suggestion.h"
#include "components/autofill/core/browser/suggestions/suggestion_type.h"
#include "components/autofill/core/common/password_form_fill_data.h"
+#include "components/password_manager/core/browser/features/password_features.h"
#include "components/password_manager/core/browser/passkey_credential.h"
#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/password_manager/core/browser/password_manager_driver.h"
@@ -592,8 +593,16 @@
ui_entry.stored_in.contains(PasswordForm::Store::kAccountStore);
const bool favicon_can_be_requested_from_google =
(is_sync_passwords_enabled || is_from_account) && !is_passphrase_user;
+ bool is_cross_domain = false;
+ if (base::FeatureList::IsEnabled(
+ password_manager::features::
+ kShowConfirmationForGroupedCredentials)) {
+ is_cross_domain = form.match_type.has_value() &&
+ password_manager_util::GetMatchType(form) ==
+ password_manager_util::GetLoginMatchType::kGrouped;
+ }
AppendManualFallbackSuggestions(
- ui_entry, on_password_form, IsCrossDomain(false),
+ ui_entry, on_password_form, IsCrossDomain(is_cross_domain),
favicon_can_be_requested_from_google, &suggestions,
Suggestion::FiltrationPolicy::kPresentOnlyWithoutFilter);
}
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 cd0c8b7d..fb8fe86 100644
--- a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
@@ -315,6 +315,12 @@
PasswordForm::MatchType::kExact);
}
+ PasswordForm grouped_password_form() const {
+ return CreateEntry("username@example.com", "password",
+ GURL("https://google.com/"),
+ PasswordForm::MatchType::kGrouped);
+ }
+
PasswordForm password_form_no_username() const {
return CreateEntry("", "password", GURL("https://google.com/"),
PasswordForm::MatchType::kExact);
@@ -876,6 +882,49 @@
}
TEST_F(PasswordSuggestionGeneratorTest,
+ ManualFallback_GroupedCredential_IsCrossDomain) {
+ std::vector<Suggestion> suggestions = GenerateSuggestedPasswordsSection(
+ {grouped_password_form()}, IsTriggeredOnPasswordForm(true));
+
+ EXPECT_THAT(suggestions,
+ ElementsAre(EqualsManualFallbackSuggestion(
+ SuggestionType::kPasswordEntry, u"google.com",
+ u"username@example.com", Suggestion::Icon::kGlobe,
+ /*is_acceptable=*/true,
+ Suggestion::FaviconDetails(
+ /*domain_url=*/GURL("https://google.com")),
+ Suggestion::PasswordSuggestionDetails(
+ u"username@example.com", u"password",
+ "https://google.com/", u"google.com",
+ /*is_cross_domain=*/true)),
+ EqualsSuggestion(SuggestionType::kSeparator),
+ EqualsManagePasswordsSuggestion()));
+}
+
+TEST_F(PasswordSuggestionGeneratorTest,
+ ManualFallback_GroupedCredential_IsNotCrossDomainWhenFeatureDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kShowConfirmationForGroupedCredentials);
+ std::vector<Suggestion> suggestions = GenerateSuggestedPasswordsSection(
+ {grouped_password_form()}, IsTriggeredOnPasswordForm(true));
+
+ EXPECT_THAT(suggestions,
+ ElementsAre(EqualsManualFallbackSuggestion(
+ SuggestionType::kPasswordEntry, u"google.com",
+ u"username@example.com", Suggestion::Icon::kGlobe,
+ /*is_acceptable=*/true,
+ Suggestion::FaviconDetails(
+ /*domain_url=*/GURL("https://google.com")),
+ Suggestion::PasswordSuggestionDetails(
+ u"username@example.com", u"password",
+ "https://google.com/", u"google.com",
+ /*is_cross_domain=*/false)),
+ EqualsSuggestion(SuggestionType::kSeparator),
+ EqualsManagePasswordsSuggestion()));
+}
+
+TEST_F(PasswordSuggestionGeneratorTest,
ManualFallback_AllPasswords_SuggestionContent) {
std::vector<Suggestion> suggestions = GenerateAllPasswordsSection(
{credential_ui_entry()}, IsTriggeredOnPasswordForm(true));
Regression Test / PoC
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 cd0c8b7d..fb8fe86 100644
--- a/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
+++ b/components/password_manager/core/browser/password_suggestion_generator_unittest.cc
@@ -315,6 +315,12 @@
PasswordForm::MatchType::kExact);
}
+ PasswordForm grouped_password_form() const {
+ return CreateEntry("username@example.com", "password",
+ GURL("https://google.com/"),
+ PasswordForm::MatchType::kGrouped);
+ }
+
PasswordForm password_form_no_username() const {
return CreateEntry("", "password", GURL("https://google.com/"),
PasswordForm::MatchType::kExact);
@@ -876,6 +882,49 @@
}
TEST_F(PasswordSuggestionGeneratorTest,
+ ManualFallback_GroupedCredential_IsCrossDomain) {
+ std::vector<Suggestion> suggestions = GenerateSuggestedPasswordsSection(
+ {grouped_password_form()}, IsTriggeredOnPasswordForm(true));
+
+ EXPECT_THAT(suggestions,
+ ElementsAre(EqualsManualFallbackSuggestion(
+ SuggestionType::kPasswordEntry, u"google.com",
+ u"username@example.com", Suggestion::Icon::kGlobe,
+ /*is_acceptable=*/true,
+ Suggestion::FaviconDetails(
+ /*domain_url=*/GURL("https://google.com")),
+ Suggestion::PasswordSuggestionDetails(
+ u"username@example.com", u"password",
+ "https://google.com/", u"google.com",
+ /*is_cross_domain=*/true)),
+ EqualsSuggestion(SuggestionType::kSeparator),
+ EqualsManagePasswordsSuggestion()));
+}
+
+TEST_F(PasswordSuggestionGeneratorTest,
+ ManualFallback_GroupedCredential_IsNotCrossDomainWhenFeatureDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ features::kShowConfirmationForGroupedCredentials);
+ std::vector<Suggestion> suggestions = GenerateSuggestedPasswordsSection(
+ {grouped_password_form()}, IsTriggeredOnPasswordForm(true));
+
+ EXPECT_THAT(suggestions,
+ ElementsAre(EqualsManualFallbackSuggestion(
+ SuggestionType::kPasswordEntry, u"google.com",
+ u"username@example.com", Suggestion::Icon::kGlobe,
+ /*is_acceptable=*/true,
+ Suggestion::FaviconDetails(
+ /*domain_url=*/GURL("https://google.com")),
+ Suggestion::PasswordSuggestionDetails(
+ u"username@example.com", u"password",
+ "https://google.com/", u"google.com",
+ /*is_cross_domain=*/false)),
+ EqualsSuggestion(SuggestionType::kSeparator),
+ EqualsManagePasswordsSuggestion()));
+}
+
+TEST_F(PasswordSuggestionGeneratorTest,
ManualFallback_AllPasswords_SuggestionContent) {
std::vector<Suggestion> suggestions = GenerateAllPasswordsSection(
{credential_ui_entry()}, IsTriggeredOnPasswordForm(true));
Original Bug Report
Potential bypass of cross-domain confirmation for grouped credentials in Manual Fallback
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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: The password manual fallback flow incorrectly marks weakly-affiliated (grouped) credentials as non-cross-domain, bypassing a mandatory security confirmation popup. This allows a site within an affiliation group to obtain credentials for other domains in the same group without the user seeing a cross-origin warning. This logic flaw potentially results in cross-origin disclosure of plaintext credentials to a compromised renderer.
Affected files:
components/password_manager/core/browser/password_suggestion_generator.cccomponents/password_manager/core/browser/password_manual_fallback_flow.cc
Estimated timestamp from git blame: 2024-04-17
Technical Description
A logic error in the password manual fallback flow allows ‘grouped’ credentials (credentials from domains that are weakly affiliated, such as different domains owned by the same parent company) to be filled without the mandatory user consent via a cross-domain confirmation popup.
Normally, when a credential is used across different domains, Chromium enforces a ShowCrossDomainConfirmationPopup to warn the user. The manual fallback flow bypasses this security gate due to how suggestions are flagged during generation.
1. Inclusion of Grouped Credentials
In PasswordManualFallbackFlow, the FormFetcher is configured to include grouped credentials in the search results:
// components/password_manager/core/browser/password_manual_fallback_flow.cc
form_fetcher_->set_filter_grouped_credentials(false);
2. Incorrect Security Flagging
When generating suggestions for the ‘Suggested’ section of the manual fallback popup, PasswordSuggestionGenerator::GetManualFallbackSuggestions hard-codes the is_cross_domain property to false for all entries in this section, even if they belong to a different (grouped) domain:
// components/password_manager/core/browser/password_suggestion_generator.cc
for (const auto& form : suggested_credentials) {
// ...
AppendManualFallbackSuggestions(
ui_entry, on_password_form, IsCrossDomain(false), // <--- Hard-coded false
favicon_can_be_requested_from_google, &suggestions,
Suggestion::FiltrationPolicy::kPresentOnlyWithoutFilter);
}
3. Bypass of the Consent Gate
The PasswordManualFallbackFlow relies on this is_cross_domain flag to decide whether to show the confirmation popup. Because the flag is incorrectly set to false, the security interstitial is skipped during selection:
// components/password_manager/core/browser/password_manual_fallback_flow.cc
void PasswordManualFallbackFlow::EnsureCrossDomainPasswordUsageGetsConsent(...) {
if (payload.is_cross_domain) {
// ... shows confirmation popup ...
return;
}
std::move(on_allowed).Run(); // <--- Falls through for grouped credentials
}
Potential Attack Scenario (Unverified)
An attacker who has compromised a renderer process (e.g., via a V8 exploit) could potentially follow these steps:
- Ensure the user has a saved password for Origin B (e.g.,
brand-b.com). - From a compromised renderer hosting Origin A (e.g.,
brand-a.com), which shares a weak affiliation group with Origin B, send amojom::AutofillDriver::AskForValuesToFillIPC withtrigger_sourceset tokManualFallbackPasswords. - The browser presents the user with a ‘Suggested’ credential for Origin B while they are on Origin A.
- If the user selects this suggestion, the browser immediately fills the plaintext password into Origin A without the intended cross-domain warning, disclosing the credential to the attacker.
Suggested Fix
The PasswordSuggestionGenerator::GetManualFallbackSuggestions should accurately determine if a suggested credential is cross-domain (including grouped matches) rather than hard-coding the value to false. Additionally, PasswordManualFallbackFlow::EnsureCrossDomainPasswordUsageGetsConsent should ideally re-verify the affiliation relationship of the selected credential similar to the logic in PasswordAutofillManager::DidAcceptSuggestion.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.