CVE-2026-17831
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/password_manager/chrome_password_manager_client.cc |
modified |
Files Changed
chrome/browser/password_manager/chrome_password_manager_client.ccchrome/browser/password_manager/password_manager_browsertest.cc
Patch
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.
Regression Test / PoC
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.
Original Bug Report
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)
- An attacker compromises the renderer process for
https://attacker.com. - The user navigates the tab from
https://attacker.comtohttps://victim.com. rfh_attackerenters the BFCache (LifecycleState::kInBackForwardCache), leaving the associated Mojo pipe forPasswordGenerationDriveropen.- The compromised renderer ignores the renderer-side suspension/freeze rules and dispatches
ShowPasswordEditingPopupwith an attacker-defined coordinates bounding box and a custompassword_valuestring. - The browser processes the request, bypassing
CheckFrameNotPrerenderingsince the frame is cached and not prerendering. - 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.