Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Passwords
DescriptionInsufficient policy enforcement in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker498269651
Fix commit1b69067db7d2 (chromium/src) +39/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-04-15

Changed Functions

FunctionChangeNotes
if
components/password_manager/core/browser/password_manual_fallback_flow.cc
modified

Files Changed

  • components/password_manager/core/browser/password_manual_fallback_flow.cc
  • components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
From 1b69067db7d25ea4208e1eadf4472b4fab1a5759 Mon Sep 17 00:00:00 2001
From: Vasilii Sukhanov <vasilii@chromium.org>
Date: Wed, 08 Apr 2026 07:48:21 -0700
Subject: [PATCH] Fix cross-domain password leak via manual-fallback preview

In PasswordManualFallbackFlow::DidSelectSuggestion, when a user selects
a password suggestion, the browser process sends the cleartext password
to the renderer for previewing. If the suggestion is cross-domain, this
leak happens without consent or auth.

This CL fixes this by omitting the password in the preview message for
all the cases by sending the fake string.

Fixed: 498269651
Change-Id: Ic9546114c453f05de1030f05c7a9830b39d73038
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7735152
Commit-Queue: Vasilii Sukhanov <vasilii@chromium.org>
Reviewed-by: Anna Tsvirchkova <atsvirchkova@google.com>
Cr-Commit-Position: refs/heads/main@{#1611490}
---

diff --git a/components/password_manager/core/browser/password_manual_fallback_flow.cc b/components/password_manager/core/browser/password_manual_fallback_flow.cc
index 6fd5468..47bd86d5 100644
--- a/components/password_manager/core/browser/password_manual_fallback_flow.cc
+++ b/components/password_manager/core/browser/password_manual_fallback_flow.cc
@@ -213,12 +213,13 @@
       if (!form) {
         return;
       }
+      const auto payload =
+          suggestion.GetPayload<Suggestion::PasswordSuggestionDetails>();
       password_manager_driver_->PreviewSuggestionById(
           form->username_element_renderer_id,
           form->password_element_renderer_id,
           GetUsernameFromLabel(suggestion.labels[0][0].value),
-          suggestion.GetPayload<Suggestion::PasswordSuggestionDetails>()
-              .password);
+          std::u16string(payload.password.length(), '*'));
       break;
     }
     case autofill::SuggestionType::kPasswordFieldByFieldFilling:
diff --git a/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc b/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
index 8b51bbc..b99c653 100644
--- a/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
+++ b/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
@@ -656,7 +656,7 @@
   EXPECT_CALL(driver(), PreviewSuggestionById(form.username_element_renderer_id,
                                               form.password_element_renderer_id,
                                               std::u16string(u"username"),
-                                              std::u16string(u"password")));
+                                              std::u16string(u"********")));
   Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
       SuggestionType::kPasswordEntry, u"google.com",
       CreateTestPasswordDetails());
@@ -667,6 +667,40 @@
   flow().DidSelectSuggestion(suggestion);
 }
 
+// Test that password manual fallback suggestion is previewed without password
+// if the suggestion is cross-domain.
+TEST_F(PasswordManualFallbackFlowTest,
+       SelectFillFullFormSuggestion_CrossDomain_TriggeredOnAPasswordForm) {
+  InitializeFlow();
+  ProcessPasswordStoreUpdates();
+
+  PasswordForm form;
+  form.username_element_renderer_id = MakeFieldRendererId();
+  form.password_element_renderer_id = MakeFieldRendererId();
+  // Simulate that the field is/isn't classified as target filling password.
+  EXPECT_CALL(password_form_cache(),
+              GetPasswordForm(_, form.username_element_renderer_id))
+      .WillRepeatedly(Return(&form));
+
+  flow().RunFlow(form.username_element_renderer_id, gfx::RectF{},
+                 TextDirection::LEFT_TO_RIGHT);
+
+  // Expect that the password is empty in the preview call.
+  EXPECT_CALL(driver(), PreviewSuggestionById(form.username_element_renderer_id,
+                                              form.password_element_renderer_id,
+                                              std::u16string(u"username"),
+                                              std::u16string(u"********")));
+  Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
+      SuggestionType::kPasswordEntry, u"google.com",
+      Suggestion::PasswordSuggestionDetails(u"username", u"password",
+                                            "https://cross-domain.com/",
+                                            u"cross-domain.com",
+                                            /*is_cross_domain=*/true));
+  suggestion.labels = {{Suggestion::Text(u"username")}};
+  suggestion.acceptability = Suggestion::Acceptability::kAcceptable;
+  flow().DidSelectSuggestion(suggestion);
+}
+
 // Test that only password field is previewed if the credential doesn't have
 // a username saved for it.
 TEST_F(PasswordManualFallbackFlowTest,
@@ -687,7 +721,7 @@
   EXPECT_CALL(driver(), PreviewSuggestionById(FieldRendererId(),
                                               form.password_element_renderer_id,
                                               std::u16string(),
-                                              std::u16string(u"password")));
+                                              std::u16string(u"********")));
   Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
       SuggestionType::kPasswordEntry, u"google.com",
       CreateTestPasswordDetails());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc b/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
index 8b51bbc..b99c653 100644
--- a/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
+++ b/components/password_manager/core/browser/password_manual_fallback_flow_unittest.cc
@@ -656,7 +656,7 @@
   EXPECT_CALL(driver(), PreviewSuggestionById(form.username_element_renderer_id,
                                               form.password_element_renderer_id,
                                               std::u16string(u"username"),
-                                              std::u16string(u"password")));
+                                              std::u16string(u"********")));
   Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
       SuggestionType::kPasswordEntry, u"google.com",
       CreateTestPasswordDetails());
@@ -667,6 +667,40 @@
   flow().DidSelectSuggestion(suggestion);
 }
 
+// Test that password manual fallback suggestion is previewed without password
+// if the suggestion is cross-domain.
+TEST_F(PasswordManualFallbackFlowTest,
+       SelectFillFullFormSuggestion_CrossDomain_TriggeredOnAPasswordForm) {
+  InitializeFlow();
+  ProcessPasswordStoreUpdates();
+
+  PasswordForm form;
+  form.username_element_renderer_id = MakeFieldRendererId();
+  form.password_element_renderer_id = MakeFieldRendererId();
+  // Simulate that the field is/isn't classified as target filling password.
+  EXPECT_CALL(password_form_cache(),
+              GetPasswordForm(_, form.username_element_renderer_id))
+      .WillRepeatedly(Return(&form));
+
+  flow().RunFlow(form.username_element_renderer_id, gfx::RectF{},
+                 TextDirection::LEFT_TO_RIGHT);
+
+  // Expect that the password is empty in the preview call.
+  EXPECT_CALL(driver(), PreviewSuggestionById(form.username_element_renderer_id,
+                                              form.password_element_renderer_id,
+                                              std::u16string(u"username"),
+                                              std::u16string(u"********")));
+  Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
+      SuggestionType::kPasswordEntry, u"google.com",
+      Suggestion::PasswordSuggestionDetails(u"username", u"password",
+                                            "https://cross-domain.com/",
+                                            u"cross-domain.com",
+                                            /*is_cross_domain=*/true));
+  suggestion.labels = {{Suggestion::Text(u"username")}};
+  suggestion.acceptability = Suggestion::Acceptability::kAcceptable;
+  flow().DidSelectSuggestion(suggestion);
+}
+
 // Test that only password field is previewed if the credential doesn't have
 // a username saved for it.
 TEST_F(PasswordManualFallbackFlowTest,
@@ -687,7 +721,7 @@
   EXPECT_CALL(driver(), PreviewSuggestionById(FieldRendererId(),
                                               form.password_element_renderer_id,
                                               std::u16string(),
-                                              std::u16string(u"password")));
+                                              std::u16string(u"********")));
   Suggestion suggestion = autofill::test::CreateAutofillSuggestion(
       SuggestionType::kPasswordEntry, u"google.com",
       CreateTestPasswordDetails());
Loading diff…

Original Bug Report

reported by vm...@google.com

Cross-domain password leak via manual-fallback preview in PasswordManualFallbackFlow

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: The manual-fallback password preview path fails to apply cross-domain consent and authentication checks before sending cleartext passwords to the renderer process. A compromised renderer can trigger a manual-fallback popup and obtain cross-origin credentials when a user hovers over the suggestion UI.

Affected files:

  • components/password_manager/core/browser/password_manual_fallback_flow.cc
  • components/password_manager/core/browser/password_suggestion_generator.cc
  • components/password_manager/content/browser/content_password_manager_driver.cc

Estimated timestamp from git blame: 2024-10-16

Summary

A potential vulnerability exists in Chromium’s password manual-fallback flow that may allow a compromised renderer process to leak cleartext cross-domain passwords during the suggestion preview phase. This occurs because the DidSelectSuggestion path in the browser process lacks the cross-domain consent and device authentication gates that are correctly applied during the ‘accept’ (filling) path. Consequently, a compromised renderer can obtain sensitive credentials if the user interacts with the manual-fallback suggestion UI (e.g., by hovering over an entry or using arrow keys).

Technical Details

In PasswordManualFallbackFlow::DidSelectSuggestion (located in components/password_manager/core/browser/password_manual_fallback_flow.cc), the browser process handles user “selections” (like hovering over an item in the Autofill popup) to provide a preview of the password in the input field. However, this path does not check the is_cross_domain flag in the suggestion payload, nor does it invoke EnsureCrossDomainPasswordUsageGetsConsent or MaybeAuthenticateBeforeFilling.

The suggestion payload is generated in password_suggestion_generator.cc, which identifies cross-domain entries and includes the cleartext password in the Suggestion::PasswordSuggestionDetails. While the ‘accept’ path (DidAcceptSuggestion) correctly uses the is_cross_domain flag to trigger a confirmation dialog and device authentication, the ‘preview’ path bypasses these protections entirely.

Potential Exploitation Path

Note: These are suggested steps for a potential exploit; a working proof-of-concept has not yet been executed.

  1. Triggering the Flow: A compromised renderer sends an autofill::mojom::PasswordManagerDriver::ShowPasswordSuggestions IPC request to the browser process. It sets the trigger_source to kManualFallbackPasswords to force the manual fallback flow.
  2. Popup Positioning: The attacker specifies the bounds in the IPC request to be a rectangle directly under the user’s current or expected mouse cursor position, ensuring immediate interaction.
  3. Suggestion Retrieval: The browser’s PasswordManualFallbackFlow fetches all saved passwords from the store via SavedPasswordsPresenter, regardless of origin, and populates the Autofill popup with these cross-domain suggestions.
  4. User Interaction: The browser displays the Autofill popup exactly where the attacker requested. Because it appears beneath the user’s cursor, a hover event is immediately triggered, registering as a “selection” for preview purposes.
  5. Credential Leak: The hover event invokes PasswordManualFallbackFlow::DidSelectSuggestion. This function extracts the cleartext password from the suggestion payload and unconditionally sends it to the renderer via the PreviewPasswordSuggestionById Mojo IPC call, bypassing Site Isolation and cross-domain consent requirements.
  6. Exfiltration: The compromised renderer intercepts the incoming PreviewPasswordSuggestionById message and reads the cleartext cross-domain password from the Mojo message arguments.

Suggested Fix

To remediate this issue, the browser should not send cleartext passwords to the renderer during the preview phase for cross-domain credentials unless consent and authentication have already been provided.

Alternatively, PasswordManualFallbackFlow::DidSelectSuggestion should be updated to check the is_cross_domain flag and conditionally omit the password (e.g., passing an empty string or a masked value) when calling PreviewSuggestionById for cross-domain entries, deferring the transmission of the actual password until DidAcceptSuggestion is invoked and the necessary security gates are cleared.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results from 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.

View on issue tracker