Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Passwords
DescriptionInsufficient validation of untrusted input in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker517714728
Fix commit0d65cb9865b1 (chromium/src) +50/-7
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
chrome/browser/password_manager/chrome_password_manager_client.cc
modified

Files Changed

  • chrome/browser/password_manager/chrome_password_manager_client.cc
  • chrome/browser/password_manager/password_manager_browsertest.cc
From 0d65cb9865b1e0772a55fc8c3486baf1e57caace Mon Sep 17 00:00:00 2001
From: Maria Kazinova <[email protected]>
Date: Tue, 02 Jun 2026 07:33:47 -0700
Subject: [PATCH] [Passwords] Ignore password generation requests from BFCached frames

Messages from frames in the Back/Forward Cache could still trigger
password generation UI over a new active page.

This CL adds an `IsActive()` check to the relevant Mojo entry points in
`ChromePasswordManagerClient` to ignore messages from inactive frames.
It also refactors the checks into a helper method to reduce duplication
and adds a regression test.

Fixed: 517714728
Change-Id: Id10122097dc6cba426c7c314d7d9490f42b83757
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7895297
Reviewed-by: Oleksandr Tara <[email protected]>
Commit-Queue: Maria Kazinova <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1640154}
---

diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 3d4286e..6be3676 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -230,6 +230,16 @@
 }
 #endif  // BUILDFLAG(IS_ANDROID)
 
+// Returns true if the frame is active and not prerendering.
+// WARNING: This method will terminate the renderer process if the frame is
+// prerendering.
+bool CheckFrameActiveAndNotPrerendering(content::RenderFrameHost* rfh) {
+  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+    return false;
+  }
+  return rfh->IsActive();
+}
+
 }  // namespace
 
 // static
@@ -1514,7 +1524,7 @@
               CPMD_BAD_ORIGIN_AUTOMATIC_GENERATION_STATUS_CHANGED)) {
     return;
   }
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+  if (!CheckFrameActiveAndNotPrerendering(rfh)) {
     return;
   }
   password_manager::ContentPasswordManagerDriver* driver =
@@ -1591,7 +1601,7 @@
           BadMessageReason::CPMD_BAD_ORIGIN_PRESAVE_GENERATED_PASSWORD)) {
     return;
   }
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+  if (!CheckFrameActiveAndNotPrerendering(rfh)) {
     return;
   }
 
@@ -1625,7 +1635,7 @@
           BadMessageReason::CPMD_BAD_ORIGIN_PASSWORD_NO_LONGER_GENERATED)) {
     return;
   }
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+  if (!CheckFrameActiveAndNotPrerendering(rfh)) {
     return;
   }
   PasswordManagerDriver* driver =
@@ -1657,7 +1667,7 @@
     const std::u16string& password_value) {
   content::RenderFrameHost* rfh =
       password_generation_driver_receivers_.GetCurrentTargetFrame();
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+  if (!CheckFrameActiveAndNotPrerendering(rfh)) {
     return;
   }
   if (!password_manager::bad_message::CheckGeneratedPassword(rfh,
@@ -1696,7 +1706,7 @@
 void ChromePasswordManagerClient::PasswordGenerationRejectedByTyping() {
   content::RenderFrameHost* rfh =
       password_generation_driver_receivers_.GetCurrentTargetFrame();
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+  if (!CheckFrameActiveAndNotPrerendering(rfh)) {
     return;
   }
   if (popup_controller_) {
@@ -1707,7 +1717,7 @@
 void ChromePasswordManagerClient::FrameWasScrolled() {
   content::RenderFrameHost* rfh =
       password_generation_driver_receivers_.GetCurrentTargetFrame();
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+  if (!CheckFrameActiveAndNotPrerendering(rfh)) {
     return;
   }
   if (popup_controller_) {
@@ -1718,7 +1728,7 @@
 void ChromePasswordManagerClient::GenerationElementLostFocus() {
   content::RenderFrameHost* rfh =
       password_generation_driver_receivers_.GetCurrentTargetFrame();
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
+  if (!CheckFrameActiveAndNotPrerendering(rfh)) {
     return;
   }
   // TODO(crbug.com/40629608): Look into removing this since FocusedInputChanged
diff --git a/chrome/browser/password_manager/password_manager_browsertest.cc b/chrome/browser/password_manager/password_manager_browsertest.cc
index b9d6385..e24da21 100644
--- a/chrome/browser/password_manager/password_manager_browsertest.cc
+++ b/chrome/browser/password_manager/password_manager_browsertest.cc
@@ -3840,6 +3840,39 @@
   ASSERT_TRUE(IsGetCredentialsSuccessful());
 }
 
+IN_PROC_BROWSER_TEST_F(PasswordManagerBackForwardCacheBrowserTest,
+                       NoGenerationPopupFromBFCachedFrame) {
+  // Navigate to a page with a password form eligible for password generation.
+  NavigateToFile("/password/signup_form.html");
+  content::RenderFrameHostWrapper rfh(WebContents()->GetPrimaryMainFrame());
+
+  // Navigate away so that the password form page is stored in the cache.
+  // This puts the frame in `kInBackForwardCache` state. In this state, the
+  // frame is not active (`rfh->IsActive()` is false), but it is not
+  // prerendering either.
+  ASSERT_TRUE(NavigateToURL(
+      WebContents(), embedded_test_server()->GetURL("a.com", "/title1.html")));
+  ASSERT_EQ(rfh->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kInBackForwardCache);
+
+  ChromePasswordManagerClient* client =
+      ChromePasswordManagerClient::FromWebContents(WebContents());
+  TestGenerationPopupObserver observer;
+  client->SetTestObserver(&observer);
+
+  // Simulate a message from the cached frame.
+  client->SetCurrentTargetFrameForTesting(rfh.get());
+  client->ShowPasswordEditingPopup(gfx::RectF(), autofill::FormData(),
+                                   autofill::FieldRendererId(), u"password");
+
+  // Verify that the popup was not shown.
+  EXPECT_FALSE(observer.popup_showing());
+
+  // Clean up.
+  client->SetCurrentTargetFrameForTesting(nullptr);
+  client->SetTestObserver(nullptr);
+}
+
 IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
                        DetectFormSubmissionOnIframe) {
   // Start from a page without a password form.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/password_manager/password_manager_browsertest.cc b/chrome/browser/password_manager/password_manager_browsertest.cc
index b9d6385..e24da21 100644
--- a/chrome/browser/password_manager/password_manager_browsertest.cc
+++ b/chrome/browser/password_manager/password_manager_browsertest.cc
@@ -3840,6 +3840,39 @@
   ASSERT_TRUE(IsGetCredentialsSuccessful());
 }
 
+IN_PROC_BROWSER_TEST_F(PasswordManagerBackForwardCacheBrowserTest,
+                       NoGenerationPopupFromBFCachedFrame) {
+  // Navigate to a page with a password form eligible for password generation.
+  NavigateToFile("/password/signup_form.html");
+  content::RenderFrameHostWrapper rfh(WebContents()->GetPrimaryMainFrame());
+
+  // Navigate away so that the password form page is stored in the cache.
+  // This puts the frame in `kInBackForwardCache` state. In this state, the
+  // frame is not active (`rfh->IsActive()` is false), but it is not
+  // prerendering either.
+  ASSERT_TRUE(NavigateToURL(
+      WebContents(), embedded_test_server()->GetURL("a.com", "/title1.html")));
+  ASSERT_EQ(rfh->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kInBackForwardCache);
+
+  ChromePasswordManagerClient* client =
+      ChromePasswordManagerClient::FromWebContents(WebContents());
+  TestGenerationPopupObserver observer;
+  client->SetTestObserver(&observer);
+
+  // Simulate a message from the cached frame.
+  client->SetCurrentTargetFrameForTesting(rfh.get());
+  client->ShowPasswordEditingPopup(gfx::RectF(), autofill::FormData(),
+                                   autofill::FieldRendererId(), u"password");
+
+  // Verify that the popup was not shown.
+  EXPECT_FALSE(observer.popup_showing());
+
+  // Clean up.
+  client->SetCurrentTargetFrameForTesting(nullptr);
+  client->SetTestObserver(nullptr);
+}
+
 IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
                        DetectFormSubmissionOnIframe) {
   // Start from a page without a password form.
Loading diff…

Original Bug Report

reported by [email protected]

Cross-origin UI spoofing via BFCached frame on password-generation Mojo interface

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 logic flaw in ChromePasswordManagerClient allows a compromised renderer residing in the Back/Forward Cache (BFCache) to display a trusted native password-generation popup. Because the browser fails to verify that the calling RenderFrameHost is active, the popup can be positioned and displayed over an unrelated cross-origin page.

Affected files:

  • chrome/browser/password_manager/chrome_password_manager_client.cc

Estimated timestamp from git blame: 2024-07-24

Description and Root Cause

In ChromePasswordManagerClient, the Mojo interface autofill::mojom::PasswordGenerationDriver is bound to a RenderFrameHost via a content::RenderFrameHostReceiverSet:

content::RenderFrameHostReceiverSet<autofill::mojom::PasswordGenerationDriver>
    password_generation_driver_receivers_;

Unlike traditional AssociatedInterfaceRegistry bindings, receivers in RenderFrameHostReceiverSet only get removed upon RenderFrameDeleted. When a page navigates and enters the Back/Forward Cache (BFCache), the frame is not deleted; consequently, the receiver’s Mojo pipe remains open and active. Furthermore, no BackForwardCacheMessageFilter is installed on this receiver set.

When a browser-side handler on PasswordGenerationDriver receives a message, it performs validation checks using CheckFrameNotPrerendering in chrome/browser/password_manager/chrome_password_manager_client.cc:

void ChromePasswordManagerClient::ShowPasswordEditingPopup(...) {
  content::RenderFrameHost* rfh =
      password_generation_driver_receivers_.GetCurrentTargetFrame();
  if (!password_manager::bad_message::CheckFrameNotPrerendering(rfh)) {
    return;
  }
  ...

This validation only checks if the frame is in the kPrerendering state. It does not check if the frame is in the kInBackForwardCache state or if rfh->IsActive() is true.

Because the popup’s container view is resolved dynamically using the currently active WebContents via WebContentsObserver::web_contents()->GetNativeView(), the popup is drawn on the top-level widget of the active tab. If the tab has navigated to a different cross-origin page while the attacker’s frame is in the BFCache, the trusted native UI bubble will overlay the active (victim’s) page with attacker-controlled text and coordinates.

Potential Attack Scenario

(Note: These are potential steps; our security review is based on static code analysis and we have not run an active Proof of Concept)

  1. An attacker compromises the renderer process for https://attacker.com.
  2. The user navigates the tab from https://attacker.com to https://victim.com.
  3. rfh_attacker enters the BFCache (LifecycleState::kInBackForwardCache), leaving the associated Mojo pipe for PasswordGenerationDriver open.
  4. The compromised renderer ignores the renderer-side suspension/freeze rules and dispatches ShowPasswordEditingPopup with an attacker-defined coordinates bounding box and a custom password_value string.
  5. The browser processes the request, bypassing CheckFrameNotPrerendering since the frame is cached and not prerendering.
  6. The browser launches the trusted native password popup overlay containing the attacker’s custom string directly over https://victim.com.

Suggested Fix

To remediate this issue, ensure that the calling RenderFrameHost is active before displaying any UI or completing Mojo processing. In chrome/browser/password_manager/chrome_password_manager_client.cc, check rfh->IsActive() inside the relevant Mojo entry points (e.g., ShowPasswordEditingPopup and AutomaticGenerationAvailable):

content::RenderFrameHost* rfh =
    password_generation_driver_receivers_.GetCurrentTargetFrame();
if (!rfh || !rfh->IsActive()) {
  return;
}

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.

View on issue tracker