Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Passwords
DescriptionInappropriate implementation in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker517793801
Fix commit2b8e53587295 (chromium/src) +133/-36
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
ASSERT_TRUE
chrome/browser/password_manager/password_manager_browsertest.cc
modified

Files Changed

  • chrome/browser/password_manager/chrome_password_manager_client.cc
  • chrome/browser/password_manager/password_manager_browsertest.cc
  • components/password_manager/content/browser/BUILD.gn
  • components/password_manager/content/browser/content_password_manager_driver.cc
From 2b8e53587295bdf4ee0710f4caf83bca9c6692a1 Mon Sep 17 00:00:00 2001
From: Maria Kazinova <[email protected]>
Date: Wed, 03 Jun 2026 07:35:58 -0700
Subject: [PATCH] [Passwords] Enforce active frame check on the driver

This CL
- Introduces a shared helper function CheckFrameActiveAndNotPrerendering()
under components/password_manager/content/browser/ to check if a
RenderFrameHost is active and not prerendering to ensure that Mojo
messages received from inactive or Back/Forward Cached renderers are
dropped early, ensuring correct lifecycle validation on the browser
side.
- Integrates this active-frame check across all Mojo entry points
in ContentPasswordManagerDriver.

Fixed: 517793801
Change-Id: I49e4a5901c918d93aa0b81ba707f424a155de654
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7896096
Reviewed-by: Ioana Treib <[email protected]>
Commit-Queue: Maria Kazinova <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1640924}
---

diff --git a/chrome/browser/password_manager/chrome_password_manager_client.cc b/chrome/browser/password_manager/chrome_password_manager_client.cc
index 6be3676..25caf59 100644
--- a/chrome/browser/password_manager/chrome_password_manager_client.cc
+++ b/chrome/browser/password_manager/chrome_password_manager_client.cc
@@ -73,6 +73,7 @@
 #include "components/password_manager/content/browser/bad_message.h"
 #include "components/password_manager/content/browser/content_password_manager_driver.h"
 #include "components/password_manager/content/browser/content_password_manager_driver_factory.h"
+#include "components/password_manager/content/browser/content_password_manager_util.h"
 #include "components/password_manager/content/browser/form_meta_data.h"
 #include "components/password_manager/content/browser/password_manager_log_router_factory.h"
 #include "components/password_manager/content/browser/password_requirements_service_factory.h"
@@ -201,6 +202,7 @@
 using autofill::mojom::FocusedFieldType;
 using autofill::password_generation::PasswordGenerationType;
 using password_manager::BadMessageReason;
+using password_manager::CheckFrameActiveAndNotPrerendering;
 using password_manager::ContentPasswordManagerDriverFactory;
 using password_manager::FieldInfoManager;
 using password_manager::PasswordForm;
@@ -230,16 +232,6 @@
 }
 #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
diff --git a/chrome/browser/password_manager/password_manager_browsertest.cc b/chrome/browser/password_manager/password_manager_browsertest.cc
index e24da21..129ce16e 100644
--- a/chrome/browser/password_manager/password_manager_browsertest.cc
+++ b/chrome/browser/password_manager/password_manager_browsertest.cc
@@ -3873,6 +3873,70 @@
   client->SetTestObserver(nullptr);
 }
 
+IN_PROC_BROWSER_TEST_F(PasswordManagerBackForwardCacheBrowserTest,
+                       NoSavePasswordPromptFromBFCachedFrame) {
+  // Navigate to a page with a password form.
+  NavigateToFile("/password/password_form.html");
+  content::RenderFrameHostWrapper rfh(WebContents()->GetPrimaryMainFrame());
+
+  // Navigate away so that the password form page is stored in the cache.
+  ASSERT_TRUE(NavigateToURL(
+      WebContents(), embedded_test_server()->GetURL("a.com", "/title1.html")));
+  ASSERT_EQ(rfh->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kInBackForwardCache);
+
+  BubbleObserver prompt_observer(WebContents());
+
+  // Get the driver for the cached frame.
+  password_manager::ContentPasswordManagerDriver* driver =
+      password_manager::ContentPasswordManagerDriver::GetForRenderFrameHost(
+          rfh.get());
+  ASSERT_TRUE(driver);
+
+  // Construct form data matching the cached page form.
+  autofill::FormData form_data;
+  form_data.set_url(
+      embedded_test_server()->GetURL("/password/password_form.html"));
+  form_data.set_action(embedded_test_server()->GetURL("/password/done.html"));
+  form_data.set_name(u"testform");
+  form_data.set_id_attribute(u"testform");
+
+  autofill::FormFieldData username_field;
+  username_field.set_name(u"username_field");
+  username_field.set_id_attribute(u"username_field");
+  username_field.set_value(u"temp");
+
+  autofill::FormFieldData password_field;
+  password_field.set_name(u"password_field");
+  password_field.set_id_attribute(u"password_field");
+  password_field.set_value(u"random");
+  password_field.set_form_control_type(
+      autofill::FormControlType::kInputPassword);
+
+  form_data.set_fields({username_field, password_field});
+
+  // Simulate a form submission message from the cached frame.
+  static_cast<autofill::mojom::PasswordManagerDriver*>(driver)
+      ->PasswordFormSubmitted(form_data);
+
+  // For regression testing: if the validation check is disabled and a form
+  // manager is created, we wait for the password store fetch to complete so
+  // that the test fails immediately due to a prompt showing, rather than timing
+  // out.
+  ChromePasswordManagerClient* client =
+      ChromePasswordManagerClient::FromWebContents(WebContents());
+  if (client->GetPasswordManager()->IsPasswordFieldDetectedOnPage()) {
+    ASSERT_TRUE(base::test::RunUntil([&]() {
+      return client->GetPasswordManager()->HaveFormManagersReceivedData(driver);
+    }));
+  }
+
+  static_cast<autofill::mojom::PasswordManagerDriver*>(driver)
+      ->DynamicFormSubmission(
+          autofill::mojom::SubmissionIndicatorEvent::HTML_FORM_SUBMISSION);
+  EXPECT_FALSE(prompt_observer.IsSavePromptAvailable());
+}
+
 IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
                        DetectFormSubmissionOnIframe) {
   // Start from a page without a password form.
diff --git a/components/password_manager/content/browser/BUILD.gn b/components/password_manager/content/browser/BUILD.gn
index 6b58d489..cae005f 100644
--- a/components/password_manager/content/browser/BUILD.gn
+++ b/components/password_manager/content/browser/BUILD.gn
@@ -10,6 +10,8 @@
     "content_password_manager_driver.h",
     "content_password_manager_driver_factory.cc",
     "content_password_manager_driver_factory.h",
+    "content_password_manager_util.cc",
+    "content_password_manager_util.h",
     "form_meta_data.cc",
     "form_meta_data.h",
     "form_submission_tracker_util.cc",
diff --git a/components/password_manager/content/browser/content_password_manager_driver.cc b/components/password_manager/content/browser/content_password_manager_driver.cc
index dc54eee1..c556798f 100644
--- a/components/password_manager/content/browser/content_password_manager_driver.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver.cc
@@ -20,6 +20,7 @@
 #include "components/autofill/core/common/unique_ids.h"
 #include "components/password_manager/content/browser/bad_message.h"
 #include "components/password_manager/content/browser/content_password_manager_driver_factory.h"
+#include "components/password_manager/content/browser/content_password_manager_util.h"
 #include "components/password_manager/content/browser/form_meta_data.h"
 #include "components/password_manager/core/browser/browser_save_password_progress_logger.h"
 #include "components/password_manager/core/browser/features/password_features.h"
@@ -495,9 +496,9 @@
 
 void ContentPasswordManagerDriver::PasswordFormsParsed(
     const std::vector<autofill::FormData>& raw_forms) {
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(
-          render_frame_host_))
+  if (!CheckFrameActiveAndNotPrerendering(render_frame_host_)) {
     return;
+  }
 
   // In case we can't obtain a valid URL or a frame isn't allowed to perform an
   // operation with generated URL, don't forward anything to password manager.
@@ -523,9 +524,9 @@
 
 void ContentPasswordManagerDriver::PasswordFormsRendered(
     const std::vector<autofill::FormData>& raw_forms) {
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(
-          render_frame_host_))
+  if (!CheckFrameActiveAndNotPrerendering(render_frame_host_)) {
     return;
+  }
 
   // In case we can't obtain a valid URL or a frame isn't allowed to perform an
   // operation with generated URL, don't forward anything to password manager.
@@ -541,9 +542,9 @@
 
 void ContentPasswordManagerDriver::PasswordFormSubmitted(
     const autofill::FormData& raw_form) {
-  if (!password_manager::bad_message::CheckFrameNotPrerendering(
-          render_frame_host_))
+  if (!CheckFrameActiveAndNotPrerendering(render_frame_host_)) {
     return;
+  }
 
   // In case we can't obtain a valid URL or a frame isn't allowed to perform an
   // operation with generated URL, don't forward anything to password manager.
@@ -558,9 +559,9 @@
 
 void ContentPasswordManagerDriver::InformAboutUserInput(
     const autofill::FormData& raw_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 e24da21..129ce16e 100644
--- a/chrome/browser/password_manager/password_manager_browsertest.cc
+++ b/chrome/browser/password_manager/password_manager_browsertest.cc
@@ -3873,6 +3873,70 @@
   client->SetTestObserver(nullptr);
 }
 
+IN_PROC_BROWSER_TEST_F(PasswordManagerBackForwardCacheBrowserTest,
+                       NoSavePasswordPromptFromBFCachedFrame) {
+  // Navigate to a page with a password form.
+  NavigateToFile("/password/password_form.html");
+  content::RenderFrameHostWrapper rfh(WebContents()->GetPrimaryMainFrame());
+
+  // Navigate away so that the password form page is stored in the cache.
+  ASSERT_TRUE(NavigateToURL(
+      WebContents(), embedded_test_server()->GetURL("a.com", "/title1.html")));
+  ASSERT_EQ(rfh->GetLifecycleState(),
+            content::RenderFrameHost::LifecycleState::kInBackForwardCache);
+
+  BubbleObserver prompt_observer(WebContents());
+
+  // Get the driver for the cached frame.
+  password_manager::ContentPasswordManagerDriver* driver =
+      password_manager::ContentPasswordManagerDriver::GetForRenderFrameHost(
+          rfh.get());
+  ASSERT_TRUE(driver);
+
+  // Construct form data matching the cached page form.
+  autofill::FormData form_data;
+  form_data.set_url(
+      embedded_test_server()->GetURL("/password/password_form.html"));
+  form_data.set_action(embedded_test_server()->GetURL("/password/done.html"));
+  form_data.set_name(u"testform");
+  form_data.set_id_attribute(u"testform");
+
+  autofill::FormFieldData username_field;
+  username_field.set_name(u"username_field");
+  username_field.set_id_attribute(u"username_field");
+  username_field.set_value(u"temp");
+
+  autofill::FormFieldData password_field;
+  password_field.set_name(u"password_field");
+  password_field.set_id_attribute(u"password_field");
+  password_field.set_value(u"random");
+  password_field.set_form_control_type(
+      autofill::FormControlType::kInputPassword);
+
+  form_data.set_fields({username_field, password_field});
+
+  // Simulate a form submission message from the cached frame.
+  static_cast<autofill::mojom::PasswordManagerDriver*>(driver)
+      ->PasswordFormSubmitted(form_data);
+
+  // For regression testing: if the validation check is disabled and a form
+  // manager is created, we wait for the password store fetch to complete so
+  // that the test fails immediately due to a prompt showing, rather than timing
+  // out.
+  ChromePasswordManagerClient* client =
+      ChromePasswordManagerClient::FromWebContents(WebContents());
+  if (client->GetPasswordManager()->IsPasswordFieldDetectedOnPage()) {
+    ASSERT_TRUE(base::test::RunUntil([&]() {
+      return client->GetPasswordManager()->HaveFormManagersReceivedData(driver);
+    }));
+  }
+
+  static_cast<autofill::mojom::PasswordManagerDriver*>(driver)
+      ->DynamicFormSubmission(
+          autofill::mojom::SubmissionIndicatorEvent::HTML_FORM_SUBMISSION);
+  EXPECT_FALSE(prompt_observer.IsSavePromptAvailable());
+}
+
 IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
                        DetectFormSubmissionOnIframe) {
   // Start from a page without a password form.
Loading diff…

Original Bug Report

reported by [email protected]

Password save prompt injection via BFCached renderer

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 vulnerability in the password manager driver allows a compromised renderer in the Back/Forward Cache to bypass lifecycle validations. This enables an inactive frame to trigger a browser-side ‘Save password?’ bubble over an unrelated, active page. This could be leveraged for timing-based click-hijacking or social-engineering attacks.

Affected files:

  • components/password_manager/content/browser/content_password_manager_driver.cc
  • components/password_manager/content/browser/bad_message.cc

Estimated timestamp from git blame: 2021-07-26

Root Cause

The autofill::mojom::PasswordManagerDriver Mojo associated receiver password_manager_receiver_ bound in ContentPasswordManagerDriver lacks a BackForwardCacheMessageFilter to drop or defer messages received while the associated frame is in the Back/Forward Cache (BFCache). Additionally, the renderer-to-browser password manager IPC handlers in ContentPasswordManagerDriver (located in components/password_manager/content/browser/content_password_manager_driver.cc) only perform a lifecycle check against prerendering via bad_message::CheckFrameNotPrerendering(), but fail to check if the calling frame is active or cached:

bool CheckFrameNotPrerendering(content::RenderFrameHost* frame) {
  if (frame->GetLifecycleState() ==
      content::RenderFrameHost::LifecycleState::kPrerendering) {
    bad_message::ReceivedBadMessage(
        frame->GetProcess(), BadMessageReason::CPMD_BAD_ORIGIN_PRERENDERING);
    return false;
  }
  return true;
}

Since the BFCached frame’s state is kInBackForwardCache rather than kPrerendering, the validation passes. No active frame validation (render_frame_host_->IsActive()) is performed on the form submission IPC pathways.

Potential Attack Scenario

An attacker with compromised renderer capabilities could potentially execute the following sequence to trigger the vulnerability (please note that these are potential/suggested steps, as our automated analysis tooling cannot currently execute code or run local verification binaries):

  1. The user visits an attacker-controlled page (https://attacker.com). The page is BFCache-eligible and binds the PasswordManagerDriver associated interface.
  2. The user navigates to https://victim.com within the same tab, causing the attacker.com RenderFrameHost to enter the Back/Forward Cache.
  3. The compromised renderer hosting attacker.com bypasses the execution suspension and transmits a PasswordFormSubmitted message containing a mock login form for attacker.com.
  4. The browser processes the form, provisionally saving it, and creating a PasswordFormManager instance.
  5. The compromised renderer transmits DynamicFormSubmission(kXhrSucceeded) to finalize the login detection flow.
  6. The browser determines successful login and invokes ChromePasswordManagerClient::PromptUserToSaveOrUpdatePassword.
  7. The browser validates if it can show a bubble on the active tab’s URL (https://victim.com). Since this succeeds, the browser displays the “Save password?” bubble anchored to the active tab’s omnibox, asking the user to save credentials for https://attacker.com.

Suggested Remediation / Fix

The IPC handlers in ContentPasswordManagerDriver should verify that the calling frame is active. Similar to other sensitive browser-side handlers, the password manager driver should reject or drop messages if !render_frame_host_->IsActive() or if the frame is in the Back/Forward Cache.

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