Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Passwords
DescriptionInsufficient policy enforcement in Passwords
ComponentPasswords
Bug ClassLogic Error
Tracker513002625
Fix commitb80505dd3614 (chromium/src) +74/-42
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
while
components/password_manager/content/browser/content_password_manager_driver.cc
modified
TEST_F
components/password_manager/content/browser/content_password_manager_driver_unittest.cc
modified
MockPasswordManagerClient
components/password_manager/core/browser/password_form_filling_unittest.cc
modified
TEST_F
components/password_manager/core/browser/password_form_filling_unittest.cc
modified

Files Changed

  • components/password_manager/content/browser/content_password_manager_driver.cc
  • components/password_manager/content/browser/content_password_manager_driver.h
  • components/password_manager/content/browser/content_password_manager_driver_unittest.cc
  • components/password_manager/core/browser/password_form_filling.cc
  • components/password_manager/core/browser/password_form_filling_unittest.cc
From b80505dd3614a2ede6b009015131b1d19f7da890 Mon Sep 17 00:00:00 2001
From: Mohamed Amir Yosef <[email protected]>
Date: Mon, 18 May 2026 08:25:01 -0700
Subject: [PATCH] [PasswordManager] Fix cross-origin iframe check

The password manager's browser-side check for cross-origin iframes
incorrectly compared only the target frame's origin against the top-
level origin. In 'sandwich' scenarios (A-B-A), this allowed cleartext
passwords to be transmitted to the renderer process memory, bypassing
intended security gates.

This CL updates the check to verify the entire ancestor chain of the
target frame. It checks if any frame in the hierarchy between the target
frame and the primary main frame is cross-origin relative to the target
frame. If so, it suppresses sending cleartext passwords.

      --gtest_filter=PasswordFormFillingTest.*

Fixed: 513002625
Test: out/Default/components_unittests
Change-Id: I2b632949afc05605009f7db3de7a53e3b04d27d5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7849080
Reviewed-by: Maria Kazinova <[email protected]>
Commit-Queue: Mohamed Amir Yosef <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1632181}
---

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 28f7beb..049553e 100644
--- a/components/password_manager/content/browser/content_password_manager_driver.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver.cc
@@ -34,6 +34,7 @@
 #include "content/public/browser/context_menu_params.h"
 #include "content/public/browser/navigation_entry.h"
 #include "content/public/browser/navigation_handle.h"
+#include "content/public/browser/render_frame_host.h"
 #include "content/public/browser/render_process_host.h"
 #include "content/public/browser/render_widget_host_view.h"
 #include "content/public/browser/site_instance.h"
@@ -426,6 +427,20 @@
   return render_frame_host_->GetLastCommittedOrigin();
 }
 
+bool ContentPasswordManagerDriver::HasCrossOriginAncestor() const {
+  content::RenderFrameHost* parent =
+      render_frame_host_->GetParentOrOuterDocument();
+  const url::Origin& target_origin =
+      render_frame_host_->GetLastCommittedOrigin();
+  while (parent) {
+    if (!parent->GetLastCommittedOrigin().IsSameOriginWith(target_origin)) {
+      return true;
+    }
+    parent = parent->GetParentOrOuterDocument();
+  }
+  return false;
+}
+
 void ContentPasswordManagerDriver::CheckViewAreaVisible(
     autofill::FieldRendererId field_id,
     base::OnceCallback<void(bool)> callback) {
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 c07f921b..9e8bfb7 100644
--- a/components/password_manager/content/browser/content_password_manager_driver.h
+++ b/components/password_manager/content/browser/content_password_manager_driver.h
@@ -125,6 +125,7 @@
   int GetFrameId() const override;
   const GURL& GetLastCommittedURL() const override;
   const url::Origin& GetLastCommittedOrigin() const override;
+  bool HasCrossOriginAncestor() const override;
   void AnnotateFieldsWithParsingResult(
       const autofill::ParsingResult& parsing_result) override;
   void CheckViewAreaVisible(autofill::FieldRendererId field_id,
diff --git a/components/password_manager/content/browser/content_password_manager_driver_unittest.cc b/components/password_manager/content/browser/content_password_manager_driver_unittest.cc
index 75cd1ad0..15dec1e 100644
--- a/components/password_manager/content/browser/content_password_manager_driver_unittest.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver_unittest.cc
@@ -530,4 +530,35 @@
   base::RunLoop().RunUntilIdle();
 }
 
+TEST_F(ContentPasswordManagerDriverTest, HasCrossOriginAncestor) {
+  NavigateAndCommit(GURL("https://victim.com"));
+
+  content::RenderFrameHost* top_rfh = main_rfh();
+
+  content::RenderFrameHost* mid_rfh =
+      content::RenderFrameHostTester::For(top_rfh)->AppendChild("middle");
+  GURL mid_url("https://evil.com");
+  auto mid_navigation =
+      content::NavigationSimulator::CreateRendererInitiated(mid_url, mid_rfh);
+  mid_navigation->Commit();
+  mid_rfh = mid_navigation->GetFinalRenderFrameHost();
+
+  content::RenderFrameHost* bot_rfh =
+      content::RenderFrameHostTester::For(mid_rfh)->AppendChild("bottom");
+  GURL bot_url("https://victim.com/login");
+  auto bot_navigation =
+      content::NavigationSimulator::CreateRendererInitiated(bot_url, bot_rfh);
+  bot_navigation->Commit();
+  bot_rfh = bot_navigation->GetFinalRenderFrameHost();
+
+  ContentPasswordManagerDriver driver(bot_rfh, &password_manager_client_);
+  EXPECT_TRUE(driver.HasCrossOriginAncestor());
+
+  ContentPasswordManagerDriver top_driver(top_rfh, &password_manager_client_);
+  EXPECT_FALSE(top_driver.HasCrossOriginAncestor());
+
+  ContentPasswordManagerDriver mid_driver(mid_rfh, &password_manager_client_);
+  EXPECT_TRUE(mid_driver.HasCrossOriginAncestor());
+}
+
 }  // namespace password_manager
diff --git a/components/password_manager/core/browser/password_form_filling.cc b/components/password_manager/core/browser/password_form_filling.cc
index 593c4fd..c6e6256 100644
--- a/components/password_manager/core/browser/password_form_filling.cc
+++ b/components/password_manager/core/browser/password_form_filling.cc
@@ -236,8 +236,7 @@
   } else if (preferred_match &&
              GetMatchType(*preferred_match) == GetLoginMatchType::kGrouped) {
     wait_for_username_reason = WaitForUsernameReason::kGroupedMatch;
-  } else if (!client->GetLastCommittedOrigin().IsSameOriginWith(
-                 driver->GetLastCommittedOrigin())) {
+  } else if (driver->HasCrossOriginAncestor()) {
     wait_for_username_reason = WaitForUsernameReason::kCrossOriginIframe;
   } else if (not_sign_in_form) {
     // If the parser did not find a current password element, don't fill.
diff --git a/components/password_manager/core/browser/password_form_filling_unittest.cc b/components/password_manager/core/browser/password_form_filling_unittest.cc
index e496f55..8fc13a24 100644
--- a/components/password_manager/core/browser/password_form_filling_unittest.cc
+++ b/components/password_manager/core/browser/password_form_filling_unittest.cc
@@ -63,6 +63,7 @@
               GetLastCommittedOrigin,
               (),
               (const, override));
+  MOCK_METHOD(bool, HasCrossOriginAncestor, (), (const, override));
 };
 
 class MockPasswordManagerClient : public StubPasswordManagerClient {
@@ -510,50 +511,21 @@
 // Exclude Android and iOS, because there credentials are not filled on
 // the page load in any case.
 #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
-TEST_F(PasswordFormFillingTest, NoFillOnPageloadInCrossOriginIframe) {
+
+TEST_F(PasswordFormFillingTest, NoFillOnPageloadWithCrossOriginAncestor) {
   base::HistogramTester histogram_tester;
 
-  observed_form_.url = GURL("https://some_website.com");
-  saved_match_.url = GURL("https://some_website.com");
-  ON_CALL(client_, GetLastCommittedOrigin)
-      .WillByDefault(
-          Return(Origin::Create(GURL("https://another_website.com"))));
-  driver_origin_ = Origin::Create(GURL("https://some_website.com"));
+  ASSERT_EQ(client_.GetLastCommittedOrigin(), driver_.GetLastCommittedOrigin());
 
-  std::vector<PasswordForm> best_matches = {saved_match_};
-  const std::vector<PasswordForm> federated_matches = {};
+  // But driver has cross-origin ancestor.
+  EXPECT_CALL(driver_, HasCrossOriginAncestor).WillOnce(Return(true));
 
-  LikelyFormFilling likely_form_filling = SendFillInformationToRenderer(
-      &client_, &driver_, observed_form_, best_matches, federated_matches,
-      &saved_match_, metrics_recorder_.get(),
-      /*webauthn_suggestions_available=*/false,
-      /*suggestion_banned_fields=*/{});
-  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect, likely_form_filling);
-  histogram_tester.ExpectUniqueSample(
-      "PasswordManager.FirstWaitForUsernameReason",
-      PasswordFormMetricsRecorder::WaitForUsernameReason::kCrossOriginIframe,
-      1);
-}
-
-TEST_F(PasswordFormFillingTest, NoFillOnPageloadForOpaqueOrigin) {
-  base::HistogramTester histogram_tester;
-
-  observed_form_.url = GURL("https://some_website.com");
-  saved_match_.url = GURL("https://some_website.com");
-
-  url::Origin opaque_origin;
-  ON_CALL(driver_, GetLastCommittedOrigin)
-      .WillByDefault(ReturnRef(opaque_origin));
-
-  std::vector<PasswordForm> best_matches = {saved_match_};
-  const std::vector<PasswordForm> federated_matches = {};
-
-  LikelyFormFilling likely_form_filling = SendFillInformationToRenderer(
-      &client_, &driver_, observed_form_, best_matches, federated_matches,
-      &saved_match_, metrics_recorder_.get(),
-      /*webauthn_suggestions_available=*/false,
-      /*suggestion_banned_fields=*/{});
-  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect, likely_form_filling);
+  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect,
+            SendFillInformationToRenderer(
+                &client_, &driver_, observed_form_, {{saved_match_}},
+                federated_matches_, &saved_match_, metrics_recorder_.get(),
+                /*webauthn_suggestions_available=*/false,
+                /*suggestion_banned_fields=*/{}));
   histogram_tester.ExpectUniqueSample(
       "PasswordManager.FirstWaitForUsernameReason",
       PasswordFormMetricsRecorder::WaitForUsernameReason::kCrossOriginIframe,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/password_manager/content/browser/content_password_manager_driver_unittest.cc b/components/password_manager/content/browser/content_password_manager_driver_unittest.cc
index 75cd1ad0..15dec1e 100644
--- a/components/password_manager/content/browser/content_password_manager_driver_unittest.cc
+++ b/components/password_manager/content/browser/content_password_manager_driver_unittest.cc
@@ -530,4 +530,35 @@
   base::RunLoop().RunUntilIdle();
 }
 
+TEST_F(ContentPasswordManagerDriverTest, HasCrossOriginAncestor) {
+  NavigateAndCommit(GURL("https://victim.com"));
+
+  content::RenderFrameHost* top_rfh = main_rfh();
+
+  content::RenderFrameHost* mid_rfh =
+      content::RenderFrameHostTester::For(top_rfh)->AppendChild("middle");
+  GURL mid_url("https://evil.com");
+  auto mid_navigation =
+      content::NavigationSimulator::CreateRendererInitiated(mid_url, mid_rfh);
+  mid_navigation->Commit();
+  mid_rfh = mid_navigation->GetFinalRenderFrameHost();
+
+  content::RenderFrameHost* bot_rfh =
+      content::RenderFrameHostTester::For(mid_rfh)->AppendChild("bottom");
+  GURL bot_url("https://victim.com/login");
+  auto bot_navigation =
+      content::NavigationSimulator::CreateRendererInitiated(bot_url, bot_rfh);
+  bot_navigation->Commit();
+  bot_rfh = bot_navigation->GetFinalRenderFrameHost();
+
+  ContentPasswordManagerDriver driver(bot_rfh, &password_manager_client_);
+  EXPECT_TRUE(driver.HasCrossOriginAncestor());
+
+  ContentPasswordManagerDriver top_driver(top_rfh, &password_manager_client_);
+  EXPECT_FALSE(top_driver.HasCrossOriginAncestor());
+
+  ContentPasswordManagerDriver mid_driver(mid_rfh, &password_manager_client_);
+  EXPECT_TRUE(mid_driver.HasCrossOriginAncestor());
+}
+
 }  // namespace password_manager
diff --git a/components/password_manager/core/browser/password_form_filling_unittest.cc b/components/password_manager/core/browser/password_form_filling_unittest.cc
index e496f55..8fc13a24 100644
--- a/components/password_manager/core/browser/password_form_filling_unittest.cc
+++ b/components/password_manager/core/browser/password_form_filling_unittest.cc
@@ -63,6 +63,7 @@
               GetLastCommittedOrigin,
               (),
               (const, override));
+  MOCK_METHOD(bool, HasCrossOriginAncestor, (), (const, override));
 };
 
 class MockPasswordManagerClient : public StubPasswordManagerClient {
@@ -510,50 +511,21 @@
 // Exclude Android and iOS, because there credentials are not filled on
 // the page load in any case.
 #if !BUILDFLAG(IS_ANDROID) && !BUILDFLAG(IS_IOS)
-TEST_F(PasswordFormFillingTest, NoFillOnPageloadInCrossOriginIframe) {
+
+TEST_F(PasswordFormFillingTest, NoFillOnPageloadWithCrossOriginAncestor) {
   base::HistogramTester histogram_tester;
 
-  observed_form_.url = GURL("https://some_website.com");
-  saved_match_.url = GURL("https://some_website.com");
-  ON_CALL(client_, GetLastCommittedOrigin)
-      .WillByDefault(
-          Return(Origin::Create(GURL("https://another_website.com"))));
-  driver_origin_ = Origin::Create(GURL("https://some_website.com"));
+  ASSERT_EQ(client_.GetLastCommittedOrigin(), driver_.GetLastCommittedOrigin());
 
-  std::vector<PasswordForm> best_matches = {saved_match_};
-  const std::vector<PasswordForm> federated_matches = {};
+  // But driver has cross-origin ancestor.
+  EXPECT_CALL(driver_, HasCrossOriginAncestor).WillOnce(Return(true));
 
-  LikelyFormFilling likely_form_filling = SendFillInformationToRenderer(
-      &client_, &driver_, observed_form_, best_matches, federated_matches,
-      &saved_match_, metrics_recorder_.get(),
-      /*webauthn_suggestions_available=*/false,
-      /*suggestion_banned_fields=*/{});
-  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect, likely_form_filling);
-  histogram_tester.ExpectUniqueSample(
-      "PasswordManager.FirstWaitForUsernameReason",
-      PasswordFormMetricsRecorder::WaitForUsernameReason::kCrossOriginIframe,
-      1);
-}
-
-TEST_F(PasswordFormFillingTest, NoFillOnPageloadForOpaqueOrigin) {
-  base::HistogramTester histogram_tester;
-
-  observed_form_.url = GURL("https://some_website.com");
-  saved_match_.url = GURL("https://some_website.com");
-
-  url::Origin opaque_origin;
-  ON_CALL(driver_, GetLastCommittedOrigin)
-      .WillByDefault(ReturnRef(opaque_origin));
-
-  std::vector<PasswordForm> best_matches = {saved_match_};
-  const std::vector<PasswordForm> federated_matches = {};
-
-  LikelyFormFilling likely_form_filling = SendFillInformationToRenderer(
-      &client_, &driver_, observed_form_, best_matches, federated_matches,
-      &saved_match_, metrics_recorder_.get(),
-      /*webauthn_suggestions_available=*/false,
-      /*suggestion_banned_fields=*/{});
-  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect, likely_form_filling);
+  EXPECT_EQ(LikelyFormFilling::kFillOnAccountSelect,
+            SendFillInformationToRenderer(
+                &client_, &driver_, observed_form_, {{saved_match_}},
+                federated_matches_, &saved_match_, metrics_recorder_.get(),
+                /*webauthn_suggestions_available=*/false,
+                /*suggestion_banned_fields=*/{}));
   histogram_tester.ExpectUniqueSample(
       "PasswordManager.FirstWaitForUsernameReason",
       PasswordFormMetricsRecorder::WaitForUsernameReason::kCrossOriginIframe,
Loading diff…

Original Bug Report

reported by [email protected]

Cleartext Password Leak in Nested Cross-Origin Iframes due to Incomplete Origin Check

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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The password manager’s browser-side check for cross-origin iframes incorrectly compares only the target frame’s origin against the top-level origin. In ‘sandwich’ scenarios (A-B-A), this allows cleartext passwords to be transmitted to the renderer process memory, bypassing intended security gates. This represents a defense-in-depth failure that could be exploited in environments where Site Isolation is disabled or relaxed.

Affected files:

  • components/password_manager/core/browser/password_form_filling.cc
  • chrome/browser/password_manager/chrome_password_manager_client.cc
  • components/password_manager/content/browser/content_password_manager_driver.cc

Estimated timestamp from git blame: Unknown (Google3 checkout)

Summary

There is a potential security vulnerability in how Chromium’s password manager determines if a subframe is ‘cross-origin’ before sending cleartext passwords for autofill. The browser-side enforcement logic currently only compares the origin of the frame being filled against the origin of the primary main frame. It fails to account for intermediate ancestors in the frame tree.

Technical Details

The vulnerability exists in components/password_manager/core/browser/password_form_filling.cc. When deciding whether to wait for a username before filling (wait_for_username), the code performs the following check (lines 238-240):

} else if (!client->GetLastCommittedOrigin().IsSameOriginWith(
               driver->GetLastCommittedOrigin())) {
  wait_for_username_reason = WaitForUsernameReason::kCrossOriginIframe;

Here, client->GetLastCommittedOrigin() returns the origin of the primary main frame (chrome/browser/password_manager/chrome_password_manager_client.cc:1152), while driver->GetLastCommittedOrigin() returns the origin of the subframe where the form was detected.

In a nested frame scenario such as https://victim.com (top) -> https://attacker.com (middle) -> https://victim.com/login (bottom), both the top-level and bottom-level frames are same-origin. The browser-side logic erroneously concludes that the context is not cross-origin and sets wait_for_username to false.

Consequently, the browser proceeds to deliver PasswordFormFillData containing cleartext credentials to the renderer process via Mojo. Before transmission, autofill::MaybeClearPasswordValues (components/autofill/core/common/password_form_fill_data.cc:92) is called to sanitize the data. However, because wait_for_username is false, it returns the data unchanged, leaving the cleartext passwords intact.

While the renderer-side check in PasswordAutofillAgent::IsInCrossOriginIframeOrEmbeddedFrame correctly identifies the intermediate cross-origin ancestor and suppresses automatic filling into the DOM, the cleartext credentials have already been delivered to the renderer process’s memory. In configurations where Site Isolation is disabled (e.g., certain enterprise policies or low-memory devices), a compromised renderer for attacker.com sharing the same process could extract these credentials from memory.

Potential Step-by-Step Attack Vector

  1. An attacker compromises or hosts a site (attacker.com) and manages to have it embedded in an iframe on a victim site (victim.com).
  2. The attacker’s frame embeds a login page from the victim site: <iframe src="https://victim.com/login">.
  3. The browser, seeing the same origin for the top and bottom frames, transmits cleartext credentials for victim.com to the renderer process for the bottom iframe.
  4. If Site Isolation is off, attacker.com and the bottom victim.com frame share a process. An attacker with renderer-level compromise (e.g., via a separate V8 or memory corruption bug) reads the process memory to steal the credentials.

Suggested Fix

The browser-side check in SendFillInformationToRenderer should be updated to verify the entire ancestor chain of the target frame. It should check if any frame in the hierarchy between the target frame and the primary main frame is cross-origin relative to the target frame. This would align the browser-side enforcement with the more robust renderer-side checks.

Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e


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