High watch CVSS 3.1 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Passwords
DescriptionInsufficient policy enforcement in Passwords
ComponentPasswords
Bug ClassPolicy Bypass
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker498269651
Fix commit1b69067db7d2 (chromium/src) +39/-4
CWECWE-284
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-04-08 (public git)
Disclosed2026-04-15
Patch gap7 days — fix public in source before the advisory

Background

Manual fallback flow
PasswordManualFallbackFlow lets a user manually invoke the password manager on a field that was not automatically recognized as a credential field, including credentials saved for other domains.
Preview
When a user hovers/selects a suggestion, the browser process sends the candidate values to the renderer so the field shows a greyed-out preview before the user commits to filling.
`PasswordSuggestionDetails`
The payload carried by a suggestion, holding the cleartext password, username, origin, and an is_cross_domain flag.
Cross-domain suggestion
A saved credential whose origin differs from the page currently being filled, which should not have its secret revealed to that page without explicit consent.

Root Cause Analysis

In PasswordManualFallbackFlow::DidSelectSuggestion (named in the commit message; the enclosing function is not printed in the @@ hunk header), the kPasswordEntry branch built the preview message by reading the real cleartext secret out of the suggestion payload via suggestion.GetPayload<Suggestion::PasswordSuggestionDetails>().password and passing it directly to password_manager_driver_->PreviewSuggestionById. Because preview fires merely on selection (hover/arrow-key), not on an authenticated fill action, this sent the cleartext password to the renderer process hosting the current page even when the credential belonged to a different domain (is_cross_domain=true). The invariant violated is that a cross-domain secret must never be handed to a page’s renderer without consent or auth.

The fix stops sending the real secret for the preview entirely: it computes the payload once and passes std::u16string(payload.password.length(), '*') — a mask of asterisks of the same length — so the renderer can still render a dot/placeholder preview without ever receiving the plaintext. The change applies unconditionally to all preview cases, not only cross-domain ones, closing the leak by construction.

Key insight
The preview path leaked the actual cleartext password to the renderer during a no-consent selection event; the fix removes the secret from the preview message altogether by substituting a same-length '*' mask, so no real credential material ever crosses the process boundary for previewing.

Attack Path

  1. Trigger manual fallback A malicious or attacker-influenced page induces the user to invoke the password manager’s manual fallback on an input field. Ground: the flow is PasswordManualFallbackFlow::RunFlow / DidSelectSuggestion as exercised by the added test.
  2. Surface a cross-domain credential The suggestion list includes a credential saved for a different origin, carried as a PasswordSuggestionDetails with is_cross_domain=true. Ground: the regression test constructs exactly such a suggestion with origin https://cross-domain.com/.
  3. Select, do not fill Merely selecting (hovering) the suggestion calls DidSelectSuggestion, which previously forwarded the cleartext .password to PreviewSuggestionById. Ground: the removed line passed the real password; no consent/auth step gates it.
  4. Observe the leaked secret in the renderer The cleartext cross-domain password reaches the renderer hosting the current page, exposing it to page-controlled context (inferred that page script can then observe the previewed field value).

Impact Assessment

An attacker-controlled page can obtain the cleartext of a password saved for a different domain by getting the user to select (not even fill) a manual-fallback suggestion, because the secret was sent to the renderer during preview without consent or authentication. The advisory rates this High severity with impact “Insufficient policy enforcement in Passwords”; it does not report in-the-wild exploitation. The precondition is user interaction with the manual-fallback suggestion UI; renderer-side observation of the previewed value is the escalation and is (inferred).

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

Audit Directions

  • Preview vs. fill distinction
    Audit every autofill/password preview path for cases that forward real secret values to the renderer on selection/hover rather than only on an authenticated fill; preview should use masked placeholders.
  • Cross-domain gating
    Search for sites where PasswordSuggestionDetails/credential payloads are consumed without checking is_cross_domain or an equivalent consent/auth gate before data leaves the browser process.
  • Length-leak residue
    Note the mask preserves password length; review whether any preview or UI path exposes exact secret length in a way that aids guessing, and whether other masks should use a fixed length.
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