Chrome · Passwords › Passwords
CVE-2026-110146
Info Disclosure in Passwords (Passwords)
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/password_manager/content/browser/content_password_manager_driver_factory.cc |
modified | |
ifchrome/browser/password_manager/password_manager_browsertest.cc |
modified |
Files Changed
components/password_manager/core/browser/password_generation_frame_helper_unittest.cccomponents/password_manager/core/browser/password_autofill_manager_unittest.cccomponents/password_manager/core/browser/password_generation_frame_helper.cccomponents/password_manager/core/browser/password_autofill_manager.cccomponents/password_manager/content/browser/content_password_manager_driver_factory_unittest.cccomponents/password_manager/content/browser/content_password_manager_driver.hchrome/browser/password_manager/password_manager_browsertest.cccomponents/password_manager/core/browser/password_autofill_manager.hcomponents/password_manager/content/browser/content_password_manager_driver_factory.cccomponents/password_manager/content/browser/content_password_manager_driver.cccomponents/password_manager/core/browser/password_generation_frame_helper.h
Patch
From 73e5c8fd1e88131b57950712c050831e7720a4db Mon Sep 17 00:00:00 2001
From: Maria Kazinova <kazinova@google.com>
Date: Thu, 08 Oct 2026 05:48:20 -0700
Subject: [PATCH] [Passwords] Reset driver state on cross-document navigations
A cross-document navigation can reuse the frame's RenderFrameHost
(e.g. when leaving the initial empty document), so the
ContentPasswordManagerDriver is reused too. When a navigation commits a
new document (i.e. not for BFCache restores or prerender activations),
invalidate the driver's weak pointers, assign a new DriverId, and reset
its PasswordGenerationFrameHelper and PasswordAutofillManager (incl.
pending callbacks and its own suggestions), so the new document starts
with fresh state.
Also rename PasswordAutofillManager::DidNavigateMainFrame() to
DidNavigateFrame().
Fixed: 561039723
Change-Id: I2752c0d67c359f1eeede337178e93bc861a200ce
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8479979
Reviewed-by: Viktor Semeniuk <vsemeniuk@google.com>
Commit-Queue: Maria Kazinova <kazinova@google.com>
Cr-Commit-Position: refs/heads/main@{#1714396}
---
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 cfb9f7b..58c8707 100644
--- a/components/password_manager/content/browser/content_password_manager_driver.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver.cc
@@ -56,6 +56,11 @@
render_frame_host->GetSiteInstance()->RequiresDedicatedProcess());
}
+DriverId GenerateDriverId() {
+ static DriverId::Generator id_generator;
+ return id_generator.GenerateNextId();
+}
+
} // namespace
ContentPasswordManagerDriver::ContentPasswordManagerDriver(
@@ -69,8 +74,7 @@
autofill::ContentAutofillClient::FromWebContents(
content::WebContents::FromRenderFrameHost(render_frame_host)),
client) {
- static DriverId::Generator id_generator;
- id_ = id_generator.GenerateNextId();
+ id_ = GenerateDriverId();
render_frame_host_->GetRemoteAssociatedInterfaces()->GetInterface(
&password_autofill_agent_);
@@ -121,6 +125,20 @@
}
}
+void ContentPasswordManagerDriver::ResetForNewDocument() {
+ // Objects created for the previous document (most importantly
+ // `PasswordFormManager`s in the `PasswordManager`'s form cache) identify
+ // this driver by its weak pointers and its ID. Invalidate both, so they
+ // treat this driver as a different one, as if a new RenderFrameHost, and
+ // hence a new driver, had been created for the new document. Otherwise, e.g.
+ // a form manager with credentials fetched for the previous document's origin
+ // could be matched with a form of the new document (see crbug.com/561039723).
+ weak_factory_.InvalidateWeakPtrs();
+ id_ = GenerateDriverId();
+ password_generation_helper_.DidNavigateFrame();
+ password_autofill_manager_.DidNavigateFrame();
+}
+
bool ContentPasswordManagerDriver::HasValidURL(bool may_kill_renderer) {
GURL url = GetURLFromRenderFrameHost(render_frame_host_);
diff --git a/components/password_manager/content/browser/content_password_manager_driver.h b/components/password_manager/content/browser/content_password_manager_driver.h
index a1837ca9..4e547b6f 100644
--- a/components/password_manager/content/browser/content_password_manager_driver.h
+++ b/components/password_manager/content/browser/content_password_manager_driver.h
@@ -55,6 +55,13 @@
pending_receiver);
void DidNavigate();
+ // Called when a navigation commits a new document in the frame (i.e. not for
+ // BFCache restores or prerendered page activations). Such a navigation may
+ // reuse the RenderFrameHost, and hence this driver, of the previous document
+ // (e.g. the first navigation away from the initial empty document). Discards
+ // all state that belongs to the previous document.
+ void ResetForNewDocument();
+
// PasswordManagerDriver implementation.
DriverId GetId() const override;
void PropagateFillDataOnParsingCompletion(
diff --git a/components/password_manager/content/browser/content_password_manager_driver_factory.cc b/components/password_manager/content/browser/content_password_manager_driver_factory.cc
index 3e203d50..3fed1c7 100644
--- a/components/password_manager/content/browser/content_password_manager_driver_factory.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver_factory.cc
@@ -96,21 +96,31 @@
if (navigation->IsSameDocument() || !navigation->HasCommitted()) {
return;
}
- GetDriverForFrame(navigation->GetRenderFrameHost())->DidNavigate();
- if (!navigation->IsInPrimaryMainFrame()) {
+ if (navigation->IsInPrimaryMainFrame()) {
+ // Clear page specific data after main frame navigation.
+ NotifyDidNavigateMainFrame(navigation->IsRendererInitiated(),
+ navigation->GetPageTransition(),
+ navigation->WasInitiatedByLinkClick(),
+ password_client_->GetPasswordManager());
+ }
+
+ ContentPasswordManagerDriver* driver =
+ GetDriverForFrame(navigation->GetRenderFrameHost());
+ if (!driver) {
return;
}
- // Clear page specific data after main frame navigation.
- NotifyDidNavigateMainFrame(navigation->IsRendererInitiated(),
- navigation->GetPageTransition(),
- navigation->WasInitiatedByLinkClick(),
- password_client_->GetPasswordManager());
- // A committed navigation always has a live RenderFrameHost.
- CHECK(navigation->GetRenderFrameHost()->IsRenderFrameLive());
- GetDriverForFrame(navigation->GetRenderFrameHost())
- ->GetPasswordAutofillManager()
- ->DidNavigateMainFrame();
+ // A cross-document navigation may commit in the same RenderFrameHost, and
+ // hence with the same driver, as the previous document (e.g. the first
+ // navigation away from the initial empty document). The previous document's
+ // state must not be reused for the new document. BFCache restores and
+ // prerendered page activations don't create a new document but reactivate an
+ // existing one together with its driver, so the driver's state is kept.
+ if (!navigation->IsServedFromBackForwardCache() &&
+ !navigation->IsPrerenderedPageActivation()) {
+ driver->ResetForNewDocument();
+ }
+ driver->DidNavigate();
}
void ContentPasswordManagerDriverFactory::RenderFrameDeleted(
diff --git a/components/password_manager/core/browser/password_autofill_manager.cc b/components/password_manager/core/browser/password_autofill_manager.cc
index c1e1eac..66481a0 100644
--- a/components/password_manager/core/browser/password_autofill_manager.cc
+++ b/components/password_manager/core/browser/password_autofill_manager.cc
@@ -734,7 +734,15 @@
/*is_for_webauthn_request=*/false);
}
-void PasswordAutofillManager::DidNavigateMainFrame() {
+void PasswordAutofillManager::DidNavigateFrame() {
+ // Hide the suggestions shown for the previous document, but only if they were
+ // shown by `this`, so that suggestions of other frames are not affected.
+ if (autofill_client_ && last_session_id_ &&
+ last_session_id_ ==
+ autofill_client_->GetSessionIdForCurrentAutofillSuggestions()) {
+ autofill_client_->HideSuggestions(
+ autofill::SuggestionHidingReason::kStaleData, GetMainFillingProduct());
+ }
fill_data_.reset();
CancelBiometricReauthIfOngoing();
favicon_tracker_.TryCancelAll();
@@ -751,6 +759,10 @@
cross_domain_confirmation_controller_.reset();
#endif
wait_for_passkeys_timer_.Stop();
+ // Drop the callbacks created for the previous document, e.g. a pending
+ // `OnAddPasswordFillData()` call or filling a credential after the user
+ // re-authenticated.
+ weak_ptr_factory_.InvalidateWeakPtrs();
}
base::WeakPtr<PasswordAutofillManager> PasswordAutofillManager::GetWeakPtr() {
diff --git a/components/password_manager/core/browser/password_autofill_manager.h b/components/password_manager/core/browser/password_autofill_manager.h
index 7d343062..cad425f 100644
--- a/components/password_manager/core/browser/password_autofill_manager.h
+++ b/components/password_manager/core/browser/password_autofill_manager.h
@@ -128,8 +128,11 @@
base::i18n::TextDirection text_direction,
bool show_password_suggestions);
- // Called when main frame navigates. Not called for in-page navigations.
- void DidNavigateMainFrame();
+ // Called when a cross-document navigation commits in the frame of the
+ // driver. Resets the state that belongs to the previous document, including
+ // the suggestions shown by `this` and pending callbacks. Not called for
+ // in-page navigations.
+ void DidNavigateFrame();
#if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
PasswordManualFallbackMetricsRecorder&
diff --git a/components/password_manager/core/browser/password_generation_frame_helper.cc b/components/password_manager/core/browser/password_generation_frame_helper.cc
index 3ba984d7..71119e99 100644
--- a/components/password_manager/core/browser/password_generation_frame_helper.cc
+++ b/components/password_manager/core/browser/password_generation_frame_helper.cc
@@ -152,6 +152,10 @@
generation_enabled_fields_.insert(field_renderer_id);
}
+void PasswordGenerationFrameHelper::DidNavigateFrame() {
+ generation_enabled_fields_.clear();
+}
+
autofill::PasswordRequirementsSpec
PasswordGenerationFrameHelper::GetPasswordRequirementsSpec(
const GURL& last_committed_url,
diff --git a/components/password_manager/core/browser/password_generation_frame_helper.h b/components/password_manager/core/browser/password_generation_frame_helper.h
index 24e69ac..4a390fa 100644
--- a/components/password_manager/core/browser/password_generation_frame_helper.h
+++ b/components/password_manager/core/browser/password_generation_frame_helper.h
@@ -74,6 +74,10 @@
virtual void AddManualGenerationEnabledField(
autofill::FieldRendererId field_renderer_id);
+ // Called when a cross-document navigation commits in the frame of the
+ // driver. Resets the state that belongs to the previous document.
+ void DidNavigateFrame();
+
// Returns password requirements spec based on provided input.
virtual autofill::PasswordRequirementsSpec GetPasswordRequirementsSpec(
const GURL& last_committed_url,
diff --git a/chrome/browser/password_manager/password_manager_browsertest.cc b/chrome/browser/password_manager/password_manager_browsertest.cc
index 0565fd5..2a6b64f 100644
--- a/chrome/browser/password_manager/password_manager_browsertest.cc
+++ b/chrome/browser/password_manager/password_manager_browsertest.cc
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <algorithm>
#include <string>
#include <string_view>
#include <utility>
@@ -66,11 +67,13 @@
#include "components/password_manager/core/browser/http_auth_manager.h"
#include "components/password_manager/core/browser/http_auth_observer.h"
#include "components/password_manager/core/browser/password_form.h"
+#include "components/password_manager/core/browser/password_form_cache.h"
#include "components/password_manager/core/browser/password_form_manager.h"
#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/password_manager/core/browser/password_manager_driver.h"
#include "components/password_manager/core/browser/password_store/password_form_converters.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
+#include "components/password_manager/core/browser/password_store/stored_credential.h"
#include "components/password_manager/core/browser/password_store/test_password_store.h"
#include "components/password_manager/core/browser/password_string.h"
#include "components/signin/public/base/signin_buildflags.h"
@@ -104,6 +107,7 @@
#include "google_apis/gaia/gaia_switches.h"
#include "net/base/filename_util.h"
#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
@@ -114,6 +118,7 @@
#include "ui/base/window_open_disposition.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/point.h"
+#include "url/origin.h"
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "chrome/browser/password_manager/password_manager_signin_intercept_test_helper.h"
@@ -2248,6 +2253,100 @@
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
}
+// Regression test for crbug.com/561039723.
+// The first navigation of a subframe away from its initial empty document
+// reuses the RenderFrameHost, and therefore the ContentPasswordManagerDriver.
+// Password manager state created for the initial document (whose URL is set to
+// origin A by document.write()) must not be reused for the new document from
+// the same-site, but cross-origin, origin B.
+IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
+ NoStaleCredentialsAfterInitialEmptyDocumentNavigation) {
+ // B is served on a different port than A: A and B are still same-site (the
+ // port is ignored when computing the site, which is required for reusing the
+ // RenderFrameHost), but A's credential isn't a PSL match for B.
+ net::EmbeddedTestServer b_server;
+ b_server.ServeFilesFromSourceDirectory("chrome/test/data");
+ ASSERT_TRUE(b_server.Start());
+ const GURL a_url =
+ embedded_test_server()->GetURL("a.example.test", "/password/other.html");
+ const GURL b_url =
+ b_server.GetURL("b.example.test", "/password/no_form_element.html");
+ ASSERT_NE(a_url.port(), b_url.port());
+
+ // Store a credential for A.
+ PasswordForm a_credential;
+ a_credential.signon_realm = a_url.DeprecatedGetOriginAsURL().spec();
+ a_credential.url = a_url;
+ a_credential.username_value = u"a_user";
+ a_credential.password_value = PasswordString(u"a_pa55w0rd");
+ GetDefaultPasswordStore(browser()->GetProfile())
+ ->AddLogin(FromPasswordForm(a_credential));
+ WaitForPasswordStore();
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), a_url));
+
+ // A writes form-less login fields into the initial empty document of a new
+ // iframe. Form-less fields are reported with `FormRendererId()`.
+ ASSERT_TRUE(content::ExecJs(RenderFrameHost(), R"(
+ const iframe = document.createElement('iframe');
+ iframe.id = 'iframe';
+ document.body.appendChild(iframe);
+ const doc = iframe.contentDocument;
+ doc.open();
+ doc.write('<input type="text" id="username">' +
+ '<input type="password" id="password">');
+ doc.close();
+ )"));
+ content::RenderFrameHost* child = content::ChildFrameAt(RenderFrameHost(), 0);
+ ASSERT_TRUE(child);
+ const content::GlobalRenderFrameHostId child_id = child->GetGlobalId();
+
+ // Returns the form manager of the iframe's current driver that observes a
+ // form from `origin` and has finished fetching credentials.
+ auto get_manager = [&](const GURL& origin) -> PasswordFormManager* {
+ content::RenderFrameHost* rfh = content::ChildFrameAt(RenderFrameHost(), 0);
+ auto* driver =
+ rfh ? ContentPasswordManagerDriver::GetForRenderFrameHost(rfh)
+ : nullptr;
+ if (!driver) {
+ return nullptr;
+ }
+ for (const auto& manager : driver->GetPasswordManager()
+ ->GetPasswordFormCache()
+ ->GetFormManagers()) {
+ if (manager->GetDriver().get() == driver &&
+ url::IsSameOriginWith(manager->GetURL(), origin) &&
+ manager->IsFetchCompleted()) {
+ return manager.get();
+ }
+ }
+ return nullptr;
+ };
+ auto has_a_credential = [](const PasswordFormManager& manager) {
+ return std::ranges::any_of(manager.GetBestMatches(),
+ [](const StoredCredential& credential) {
+ return credential.username_value == u"a_user";
+ });
+ };
+
+ // Sanity check: the form in A's initial document gets A's credential.
+ ASSERT_TRUE(base::test::RunUntil([&] { return get_manager(a_url); }));
+ ASSERT_TRUE(has_a_credential(*get_manager(a_url)));
+
+ // A navigates the iframe to B.
+ ASSERT_TRUE(content::NavigateIframeToURL(WebContents(), "iframe", b_url));
+ child = content::ChildFrameAt(RenderFrameHost(), 0);
+ ASSERT_TRUE(child);
+ ASSERT_EQ(b_url, child->GetLastCommittedURL());
+ // Precondition of the scenario: the RenderFrameHost is reused.
+ ASSERT_EQ(child_id, child->GetGlobalId());
+
+ // B's form-less fields must get a fresh form manager that doesn't know about
+ // A's credential.
+ ASSERT_TRUE(base::test::RunUntil([&] { return get_manager(b_url); }));
+ EXPECT_FALSE(has_a_credential(*get_manager(b_url)));
+}
+
// Check that a password form in an iframe of same origin will not be
// filled in until user interact with the iframe.
IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
diff --git a/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc b/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc
index 5976273..3caeed0 100644
--- a/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc
@@ -5,21 +5,31 @@
#include "components/password_manager/content/browser/content_password_manager_driver_factory.h"
#include <memory>
+#include <optional>
#include <utility>
+#include "base/memory/weak_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "components/autofill/core/browser/foundations/test_autofill_client.h"
+#include "components/autofill/core/common/unique_ids.h"
#include "components/password_manager/content/browser/content_password_manager_driver.h"
#include "components/password_manager/content/browser/content_password_manager_driver_factory_test_api.h"
+#include "components/password_manager/core/browser/password_generation_frame_helper.h"
+#include "components/password_manager/core/browser/password_manager_driver.h"
#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_features.h"
+#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/test_utils.h"
+#include "content/public/test/web_contents_tester.h"
#include "content_password_manager_driver_factory_test_api.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
+#include "url/gurl.h"
namespace password_manager {
@@ -82,4 +92,186 @@
&factory(), fenced_frame_subframe));
}
... (file diff truncated)
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 0565fd5..2a6b64f 100644
--- a/chrome/browser/password_manager/password_manager_browsertest.cc
+++ b/chrome/browser/password_manager/password_manager_browsertest.cc
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+#include <algorithm>
#include <string>
#include <string_view>
#include <utility>
@@ -66,11 +67,13 @@
#include "components/password_manager/core/browser/http_auth_manager.h"
#include "components/password_manager/core/browser/http_auth_observer.h"
#include "components/password_manager/core/browser/password_form.h"
+#include "components/password_manager/core/browser/password_form_cache.h"
#include "components/password_manager/core/browser/password_form_manager.h"
#include "components/password_manager/core/browser/password_manager_client.h"
#include "components/password_manager/core/browser/password_manager_driver.h"
#include "components/password_manager/core/browser/password_store/password_form_converters.h"
#include "components/password_manager/core/browser/password_store/password_store_interface.h"
+#include "components/password_manager/core/browser/password_store/stored_credential.h"
#include "components/password_manager/core/browser/password_store/test_password_store.h"
#include "components/password_manager/core/browser/password_string.h"
#include "components/signin/public/base/signin_buildflags.h"
@@ -104,6 +107,7 @@
#include "google_apis/gaia/gaia_switches.h"
#include "net/base/filename_util.h"
#include "net/dns/mock_host_resolver.h"
+#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
@@ -114,6 +118,7 @@
#include "ui/base/window_open_disposition.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/point.h"
+#include "url/origin.h"
#if BUILDFLAG(ENABLE_DICE_SUPPORT)
#include "chrome/browser/password_manager/password_manager_signin_intercept_test_helper.h"
@@ -2248,6 +2253,100 @@
content::EXECUTE_SCRIPT_NO_USER_GESTURE));
}
+// Regression test for crbug.com/561039723.
+// The first navigation of a subframe away from its initial empty document
+// reuses the RenderFrameHost, and therefore the ContentPasswordManagerDriver.
+// Password manager state created for the initial document (whose URL is set to
+// origin A by document.write()) must not be reused for the new document from
+// the same-site, but cross-origin, origin B.
+IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
+ NoStaleCredentialsAfterInitialEmptyDocumentNavigation) {
+ // B is served on a different port than A: A and B are still same-site (the
+ // port is ignored when computing the site, which is required for reusing the
+ // RenderFrameHost), but A's credential isn't a PSL match for B.
+ net::EmbeddedTestServer b_server;
+ b_server.ServeFilesFromSourceDirectory("chrome/test/data");
+ ASSERT_TRUE(b_server.Start());
+ const GURL a_url =
+ embedded_test_server()->GetURL("a.example.test", "/password/other.html");
+ const GURL b_url =
+ b_server.GetURL("b.example.test", "/password/no_form_element.html");
+ ASSERT_NE(a_url.port(), b_url.port());
+
+ // Store a credential for A.
+ PasswordForm a_credential;
+ a_credential.signon_realm = a_url.DeprecatedGetOriginAsURL().spec();
+ a_credential.url = a_url;
+ a_credential.username_value = u"a_user";
+ a_credential.password_value = PasswordString(u"a_pa55w0rd");
+ GetDefaultPasswordStore(browser()->GetProfile())
+ ->AddLogin(FromPasswordForm(a_credential));
+ WaitForPasswordStore();
+
+ ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), a_url));
+
+ // A writes form-less login fields into the initial empty document of a new
+ // iframe. Form-less fields are reported with `FormRendererId()`.
+ ASSERT_TRUE(content::ExecJs(RenderFrameHost(), R"(
+ const iframe = document.createElement('iframe');
+ iframe.id = 'iframe';
+ document.body.appendChild(iframe);
+ const doc = iframe.contentDocument;
+ doc.open();
+ doc.write('<input type="text" id="username">' +
+ '<input type="password" id="password">');
+ doc.close();
+ )"));
+ content::RenderFrameHost* child = content::ChildFrameAt(RenderFrameHost(), 0);
+ ASSERT_TRUE(child);
+ const content::GlobalRenderFrameHostId child_id = child->GetGlobalId();
+
+ // Returns the form manager of the iframe's current driver that observes a
+ // form from `origin` and has finished fetching credentials.
+ auto get_manager = [&](const GURL& origin) -> PasswordFormManager* {
+ content::RenderFrameHost* rfh = content::ChildFrameAt(RenderFrameHost(), 0);
+ auto* driver =
+ rfh ? ContentPasswordManagerDriver::GetForRenderFrameHost(rfh)
+ : nullptr;
+ if (!driver) {
+ return nullptr;
+ }
+ for (const auto& manager : driver->GetPasswordManager()
+ ->GetPasswordFormCache()
+ ->GetFormManagers()) {
+ if (manager->GetDriver().get() == driver &&
+ url::IsSameOriginWith(manager->GetURL(), origin) &&
+ manager->IsFetchCompleted()) {
+ return manager.get();
+ }
+ }
+ return nullptr;
+ };
+ auto has_a_credential = [](const PasswordFormManager& manager) {
+ return std::ranges::any_of(manager.GetBestMatches(),
+ [](const StoredCredential& credential) {
+ return credential.username_value == u"a_user";
+ });
+ };
+
+ // Sanity check: the form in A's initial document gets A's credential.
+ ASSERT_TRUE(base::test::RunUntil([&] { return get_manager(a_url); }));
+ ASSERT_TRUE(has_a_credential(*get_manager(a_url)));
+
+ // A navigates the iframe to B.
+ ASSERT_TRUE(content::NavigateIframeToURL(WebContents(), "iframe", b_url));
+ child = content::ChildFrameAt(RenderFrameHost(), 0);
+ ASSERT_TRUE(child);
+ ASSERT_EQ(b_url, child->GetLastCommittedURL());
+ // Precondition of the scenario: the RenderFrameHost is reused.
+ ASSERT_EQ(child_id, child->GetGlobalId());
+
+ // B's form-less fields must get a fresh form manager that doesn't know about
+ // A's credential.
+ ASSERT_TRUE(base::test::RunUntil([&] { return get_manager(b_url); }));
+ EXPECT_FALSE(has_a_credential(*get_manager(b_url)));
+}
+
// Check that a password form in an iframe of same origin will not be
// filled in until user interact with the iframe.
IN_PROC_BROWSER_TEST_F(PasswordManagerBrowserTest,
diff --git a/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc b/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc
index 5976273..3caeed0 100644
--- a/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver_factory_unittest.cc
@@ -5,21 +5,31 @@
#include "components/password_manager/content/browser/content_password_manager_driver_factory.h"
#include <memory>
+#include <optional>
#include <utility>
+#include "base/memory/weak_ptr.h"
#include "base/test/scoped_feature_list.h"
#include "components/autofill/core/browser/foundations/test_autofill_client.h"
+#include "components/autofill/core/common/unique_ids.h"
#include "components/password_manager/content/browser/content_password_manager_driver.h"
#include "components/password_manager/content/browser/content_password_manager_driver_factory_test_api.h"
+#include "components/password_manager/core/browser/password_generation_frame_helper.h"
+#include "components/password_manager/core/browser/password_manager_driver.h"
#include "components/password_manager/core/browser/stub_password_manager_client.h"
#include "components/password_manager/core/common/password_manager_features.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_features.h"
+#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/navigation_simulator.h"
+#include "content/public/test/prerender_test_util.h"
#include "content/public/test/test_renderer_host.h"
+#include "content/public/test/test_utils.h"
+#include "content/public/test/web_contents_tester.h"
#include "content_password_manager_driver_factory_test_api.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features.h"
+#include "url/gurl.h"
namespace password_manager {
@@ -82,4 +92,186 @@
&factory(), fenced_frame_subframe));
}
+class ContentPasswordManagerDriverFactoryTest
+ : public content::RenderViewHostTestHarness {
+ public:
+ void SetUp() override {
+ content::RenderViewHostTestHarness::SetUp();
+ factory_ = ContentPasswordManagerDriverFactoryTestApi::Create(
+ web_contents(), &password_manager_client_);
+ }
+
+ void TearDown() override {
+ factory_.reset();
+ content::RenderViewHostTestHarness::TearDown();
+ }
+
+ // Navigates `rfh` to `url` and returns the RenderFrameHost that committed.
+ content::RenderFrameHost* NavigateFrame(const GURL& url,
+ content::RenderFrameHost* rfh) {
+ auto navigation =
+ content::NavigationSimulator::CreateRendererInitiated(url, rfh);
+ navigation->Commit();
+ return navigation->GetFinalRenderFrameHost();
+ }
+
+ ContentPasswordManagerDriver* GetDriver(content::RenderFrameHost* rfh) {
+ return ContentPasswordManagerDriverFactoryTestApi::GetDriverForFrame(
+ factory_.get(), rfh);
+ }
+
+ private:
+ StubPasswordManagerClient password_manager_client_;
+ std::unique_ptr<ContentPasswordManagerDriverFactory> factory_;
+};
+
+// Tests that when a cross-document navigation in a subframe reuses the
+// RenderFrameHost (and hence the driver) of the previous document, the driver
+// discards its identity and state, so that state created for the previous
+// document isn't associated with the new document. See crbug.com/561039723.
+TEST_F(ContentPasswordManagerDriverFactoryTest,
+ SubframeNavigationReusingRenderFrameHostResetsDriver) {
+ NavigateFrame(GURL("https://a.test/"), main_rfh());
+ content::RenderFrameHost* child =
+ content::RenderFrameHostTester::For(main_rfh())->AppendChild("child");
+ ContentPasswordManagerDriver* driver = GetDriver(child);
+ ASSERT_TRUE(driver);
+ const DriverId old_id = driver->GetId();
+ base::WeakPtr<PasswordManagerDriver> old_weak_ptr = driver->AsWeakPtr();
+ driver->GetPasswordGenerationHelper()->AddManualGenerationEnabledField(
+ autofill::FieldRendererId(1));
+
+ // The first navigation away from the initial empty document reuses the
+ // RenderFrameHost.
+ ASSERT_EQ(child, NavigateFrame(GURL("https://b.a.test/"), child));
+ ASSERT_EQ(driver, GetDriver(child));
+
+ EXPECT_FALSE(old_weak_ptr);
+ EXPECT_NE(old_id, driver->GetId());
+ EXPECT_TRUE(driver->AsWeakPtr());
+ EXPECT_TRUE(driver->GetPasswordGenerationHelper()
+ ->GenerationEnabledFieldsForTests()
+ .empty());
+}
+
+// Tests that when a cross-document navigation in the main frame reuses the
+// RenderFrameHost (and hence the driver) of the previous document, the driver
+// is reset as well.
+TEST_F(ContentPasswordManagerDriverFactoryTest,
+ MainFrameNavigationReusingRenderFrameHostResetsDriver) {
+ content::RenderFrameHost* rfh = main_rfh();
+ content::RenderFrameHostTester::For(rfh)->InitializeRenderFrameIfNeeded();
+ ContentPasswordManagerDriver* driver = GetDriver(rfh);
+ ASSERT_TRUE(driver);
+ const DriverId old_id = driver->GetId();
+ base::WeakPtr<PasswordManagerDriver> old_weak_ptr = driver->AsWeakPtr();
+
+ // The first navigation away from the initial empty document reuses the
+ // RenderFrameHost.
+ ASSERT_EQ(rfh, NavigateFrame(GURL("https://a.test/"), rfh));
+ ASSERT_EQ(driver, GetDriver(rfh));
+
+ EXPECT_FALSE(old_weak_ptr);
+ EXPECT_NE(old_id, driver->GetId());
+ EXPECT_TRUE(driver->AsWeakPtr());
+}
+
+// Tests that same-document navigations in a subframe don't reset the driver.
+TEST_F(ContentPasswordManagerDriverFactoryTest,
+ SameDocumentSubframeNavigationDoesNotResetDriver) {
+ NavigateFrame(GURL("https://a.test/"), main_rfh());
+ content::RenderFrameHost* child =
+ content::RenderFrameHostTester::For(main_rfh())->AppendChild("child");
+ child = NavigateFrame(GURL("https://a.test/child"), child);
+ ContentPasswordManagerDriver* driver = GetDriver(child);
+ ASSERT_TRUE(driver);
+ const DriverId old_id = driver->GetId();
+ base::WeakPtr<PasswordManagerDriver> old_weak_ptr = driver->AsWeakPtr();
+
+ auto same_document_navigation =
+ content::NavigationSimulator::CreateRendererInitiated(
+ GURL("https://a.test/child#ref"), child);
+ same_document_navigation->CommitSameDocument();
+ ASSERT_EQ(child, same_document_navigation->GetFinalRenderFrameHost());
+
+ EXPECT_TRUE(old_weak_ptr);
+ EXPECT_EQ(old_id, driver->GetId());
+}
+
+// Fixture for testing page activations, i.e. BFCache restores and prerendered
+// page activations.
+class ContentPasswordManagerDriverFactoryPageActivationTest
+ : public ContentPasswordManagerDriverFactoryTest {
+ public:
+ ContentPasswordManagerDriverFactoryPageActivationTest() {
+ bfcache_feature_list_.InitWithFeaturesAndParameters(
+ content::GetDefaultEnabledBackForwardCacheFeaturesForTesting(),
+ content::GetDefaultDisabledBackForwardCacheFeaturesForTesting());
+ }
+
... (truncated)
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page