CVE-2026-17834
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ASSERT_TRUEchrome/browser/password_manager/password_manager_browsertest.cc |
modified |
Files Changed
chrome/browser/password_manager/chrome_password_manager_client.ccchrome/browser/password_manager/password_manager_browsertest.cccomponents/password_manager/content/browser/BUILD.gncomponents/password_manager/content/browser/content_password_manager_driver.cc
Patch
From 2b8e53587295bdf4ee0710f4caf83bca9c6692a1 Mon Sep 17 00:00:00 2001
From: Maria Kazinova <kazinova@google.com>
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 <ioanap@chromium.org>
Commit-Queue: Maria Kazinova <kazinova@google.com>
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) {
Regression Test / PoC
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.
Original Bug Report
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.cccomponents/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):
- The user visits an attacker-controlled page (
https://attacker.com). The page is BFCache-eligible and binds thePasswordManagerDriverassociated interface. - The user navigates to
https://victim.comwithin the same tab, causing theattacker.comRenderFrameHostto enter the Back/Forward Cache. - The compromised renderer hosting
attacker.combypasses the execution suspension and transmits aPasswordFormSubmittedmessage containing a mock login form forattacker.com. - The browser processes the form, provisionally saving it, and creating a
PasswordFormManagerinstance. - The compromised renderer transmits
DynamicFormSubmission(kXhrSucceeded)to finalize the login detection flow. - The browser determines successful login and invokes
ChromePasswordManagerClient::PromptUserToSaveOrUpdatePassword. - 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 forhttps://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.