Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Core
DescriptionInformation leak in Core
ComponentCore
Bug ClassLogic Error
Tracker498327743
Fix commitcb63f7f190a0 (chromium/src) +99/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-08-25

Changed Functions

FunctionChangeNotes
if
content/browser/renderer_host/mixed_content_checker.cc
modified
MixedContentCheckerShouldBlockNavigationWithBlockableContextTest
content/browser/renderer_host/mixed_content_checker_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/mixed_content_checker.cc
  • content/browser/renderer_host/mixed_content_checker_unittest.cc
From cb63f7f190a035064508206f10a23e869f835095 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <[email protected]>
Date: Tue, 21 Jul 2026 06:59:54 -0700
Subject: [PATCH] MixedContent: Report ancestor origin to cross-process renderers

When the browser-side MixedContentChecker reports mixed content for a
subframe navigation to the navigating frame's renderer via the
MixedContentFound IPC, send only the origin of the mixed content frame
as the main resource URL when that frame is hosted in a different
renderer process. This mirrors the renderer-side logic in
blink::MainResourceUrlForFrame(), which already uses the origin for
remote frames.

When the mixed content frame is in the same process as the navigating
frame, the full URL is still sent so that the console message remains as
descriptive as before.

Fixed: 498327743
Change-Id: Iefec3a544354b7de52851804900647f356ef710d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8122298
Reviewed-by: Alex Moshchuk <[email protected]>
Commit-Queue: Andrew Paseltiner <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1665445}
---

diff --git a/content/browser/renderer_host/mixed_content_checker.cc b/content/browser/renderer_host/mixed_content_checker.cc
index 9c93765..aa57b0d9 100644
--- a/content/browser/renderer_host/mixed_content_checker.cc
+++ b/content/browser/renderer_host/mixed_content_checker.cc
@@ -98,7 +98,7 @@
 }
 
 void UpdateRendererOnMixedContentFound(NavigationRequest* navigation_request,
-                                       const GURL& mixed_content_url,
+                                       RenderFrameHostImpl* mixed_content_frame,
                                        bool was_allowed,
                                        bool for_redirect) {
   // TODO(carlosk): the root node should never be considered as being/having
@@ -109,6 +109,14 @@
 
   RenderFrameHostImpl* rfh =
       navigation_request->frame_tree_node()->current_frame_host();
+  // Mirrors `blink::MainResourceUrlForFrame()`. When the mixed content frame
+  // is in a different process from the navigating frame, only send its origin
+  // since the renderer should not have access to its full URL.
+  GURL mixed_content_url =
+      mixed_content_frame->GetSiteInstance()->group() ==
+              rfh->GetSiteInstance()->group()
+          ? mixed_content_frame->GetLastCommittedURL()
+          : mixed_content_frame->GetLastCommittedOrigin().GetURL();
   DCHECK(!navigation_request->GetRedirectChain().empty());
   GURL url_before_redirects = navigation_request->GetRedirectChain()[0];
   rfh->GetAssociatedLocalFrame()->MixedContentFound(
@@ -217,9 +225,9 @@
       &navigation_mixed_content_features_, &should_report_to_renderer);
 
   if (should_report_to_renderer) {
-    UpdateRendererOnMixedContentFound(
-        request, mixed_content_frame->GetLastCommittedURL(),
-        /*was_allowed=*/!should_block, for_redirect);
+    UpdateRendererOnMixedContentFound(request, mixed_content_frame,
+                                      /*was_allowed=*/!should_block,
+                                      for_redirect);
     MaybeSendBlinkFeatureUsageReport(navigation_handle,
                                      navigation_mixed_content_features_);
   }
diff --git a/content/browser/renderer_host/mixed_content_checker_unittest.cc b/content/browser/renderer_host/mixed_content_checker_unittest.cc
index 1e64dfa..7f6b1df 100644
--- a/content/browser/renderer_host/mixed_content_checker_unittest.cc
+++ b/content/browser/renderer_host/mixed_content_checker_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/fake_local_frame.h"
+#include "content/public/test/test_utils.h"
 #include "content/test/navigation_simulator_impl.h"
 #include "content/test/test_render_frame_host.h"
 #include "content/test/test_render_view_host.h"
@@ -267,6 +268,92 @@
   EXPECT_THAT(inspector->reported_web_features(), IsEmpty());
 }
 
+// When mixed content is found in a navigation initiated from a subframe that
+// is hosted in a different process from the mixed content frame, the main
+// resource URL reported to the subframe's renderer should only contain the
+// origin of the mixed content frame, not its full URL. The mixed content frame
+// is the main frame in this scenario.
+TEST_P(MixedContentCheckerShouldBlockNavigationTest,
+       ReportsAncestorOriginToCrossProcessRenderer) {
+  if (!AreAllSitesIsolatedForTesting()) {
+    GTEST_SKIP() << "Site isolation is required for this test.";
+  }
+
+  const GURL main_frame_url("https://source.com/private/path?token=value#frag");
+  NavigateAndCommit(main_frame_url);
+  TestRenderFrameHost* main_rfh = main_test_rfh();
+  main_rfh->DidEnforceInsecureRequestPolicy(
+      blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone);
+
+  TestRenderFrameHost* subframe = static_cast<TestRenderFrameHost*>(
+      NavigationSimulator::NavigateAndCommitFromDocument(
+          GURL("https://other.com/subframe"),
+          main_rfh->AppendChild("subframe")));
+  ASSERT_NE(subframe->GetProcess(), main_rfh->GetProcess());
+  auto interceptor = std::make_unique<LocalFrameInterceptor>(subframe);
+
+  std::unique_ptr<NavigationSimulatorImpl> navigation =
+      NavigationSimulatorImpl::CreateRendererInitiated(
+          GURL("http://target.com"), subframe);
+  navigation->SetReferrer(blink::mojom::Referrer::New(
+      subframe->GetLastCommittedURL(),
+      network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin));
+  navigation->set_request_context_type(
+      blink::mojom::RequestContextType::INTERNAL);
+  navigation->set_mixed_content_context_type(
+      blink::mojom::MixedContentContextType::kBlockable);
+  navigation->Start();
+
+  auto checker = MixedContentChecker();
+  EXPECT_TRUE(checker.ShouldBlockNavigation(*navigation->GetNavigationHandle(),
+                                            for_redirect()));
+  interceptor->FlushLocalFrameMessages();
+  EXPECT_THAT(
+      interceptor->mixed_content_result(),
+      Optional(FieldsAre(GURL("https://source.com/"), GURL("http://target.com"),
+                         /*was_allowed=*/false, for_redirect())));
+}
+
+// When mixed content is found in a navigation initiated from a subframe that
+// is hosted in the same process as the mixed content frame, the full URL of
+// the mixed content frame is reported to the renderer (which already has
+// access to it). The mixed content frame is the main frame in this scenario.
+TEST_P(MixedContentCheckerShouldBlockNavigationTest,
+       ReportsAncestorFullUrlToSameProcessRenderer) {
+  const GURL main_frame_url("https://source.com/private/path?token=value#frag");
+  NavigateAndCommit(main_frame_url);
+  TestRenderFrameHost* main_rfh = main_test_rfh();
+  main_rfh->DidEnforceInsecureRequestPolicy(
+      blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone);
+
+  TestRenderFrameHost* subframe = static_cast<TestRenderFrameHost*>(
+      NavigationSimulator::NavigateAndCommitFromDocument(
+          GURL("https://source.com/subframe"),
+          main_rfh->AppendChild("subframe")));
+  ASSERT_EQ(subframe->GetProcess(), main_rfh->GetProcess());
+  auto interceptor = std::make_unique<LocalFrameInterceptor>(subframe);
+
+  std::unique_ptr<NavigationSimulatorImpl> navigation =
+      NavigationSimulatorImpl::CreateRendererInitiated(
+          GURL("http://target.com"), subframe);
+  navigation->SetReferrer(blink::mojom::Referrer::New(
+      subframe->GetLastCommittedURL(),
+      network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin));
+  navigation->set_request_context_type(
+      blink::mojom::RequestContextType::INTERNAL);
+  navigation->set_mixed_content_context_type(
+      blink::mojom::MixedContentContextType::kBlockable);
+  navigation->Start();
+
+  auto checker = MixedContentChecker();
+  EXPECT_TRUE(checker.ShouldBlockNavigation(*navigation->GetNavigationHandle(),
+                                            for_redirect()));
+  interceptor->FlushLocalFrameMessages();
+  EXPECT_THAT(interceptor->mixed_content_result(),
+              Optional(FieldsAre(main_frame_url, GURL("http://target.com"),
+                                 /*was_allowed=*/false, for_redirect())));
+}
+
 // Tests to cover MixedContentContextType = kBlockable.
 class MixedContentCheckerShouldBlockNavigationWithBlockableContextTest
     : public MixedContentCheckerShouldBlockNavigationTestBase {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/mixed_content_checker_unittest.cc b/content/browser/renderer_host/mixed_content_checker_unittest.cc
index 1e64dfa..7f6b1df 100644
--- a/content/browser/renderer_host/mixed_content_checker_unittest.cc
+++ b/content/browser/renderer_host/mixed_content_checker_unittest.cc
@@ -13,6 +13,7 @@
 #include "base/test/scoped_feature_list.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/test/fake_local_frame.h"
+#include "content/public/test/test_utils.h"
 #include "content/test/navigation_simulator_impl.h"
 #include "content/test/test_render_frame_host.h"
 #include "content/test/test_render_view_host.h"
@@ -267,6 +268,92 @@
   EXPECT_THAT(inspector->reported_web_features(), IsEmpty());
 }
 
+// When mixed content is found in a navigation initiated from a subframe that
+// is hosted in a different process from the mixed content frame, the main
+// resource URL reported to the subframe's renderer should only contain the
+// origin of the mixed content frame, not its full URL. The mixed content frame
+// is the main frame in this scenario.
+TEST_P(MixedContentCheckerShouldBlockNavigationTest,
+       ReportsAncestorOriginToCrossProcessRenderer) {
+  if (!AreAllSitesIsolatedForTesting()) {
+    GTEST_SKIP() << "Site isolation is required for this test.";
+  }
+
+  const GURL main_frame_url("https://source.com/private/path?token=value#frag");
+  NavigateAndCommit(main_frame_url);
+  TestRenderFrameHost* main_rfh = main_test_rfh();
+  main_rfh->DidEnforceInsecureRequestPolicy(
+      blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone);
+
+  TestRenderFrameHost* subframe = static_cast<TestRenderFrameHost*>(
+      NavigationSimulator::NavigateAndCommitFromDocument(
+          GURL("https://other.com/subframe"),
+          main_rfh->AppendChild("subframe")));
+  ASSERT_NE(subframe->GetProcess(), main_rfh->GetProcess());
+  auto interceptor = std::make_unique<LocalFrameInterceptor>(subframe);
+
+  std::unique_ptr<NavigationSimulatorImpl> navigation =
+      NavigationSimulatorImpl::CreateRendererInitiated(
+          GURL("http://target.com"), subframe);
+  navigation->SetReferrer(blink::mojom::Referrer::New(
+      subframe->GetLastCommittedURL(),
+      network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin));
+  navigation->set_request_context_type(
+      blink::mojom::RequestContextType::INTERNAL);
+  navigation->set_mixed_content_context_type(
+      blink::mojom::MixedContentContextType::kBlockable);
+  navigation->Start();
+
+  auto checker = MixedContentChecker();
+  EXPECT_TRUE(checker.ShouldBlockNavigation(*navigation->GetNavigationHandle(),
+                                            for_redirect()));
+  interceptor->FlushLocalFrameMessages();
+  EXPECT_THAT(
+      interceptor->mixed_content_result(),
+      Optional(FieldsAre(GURL("https://source.com/"), GURL("http://target.com"),
+                         /*was_allowed=*/false, for_redirect())));
+}
+
+// When mixed content is found in a navigation initiated from a subframe that
+// is hosted in the same process as the mixed content frame, the full URL of
+// the mixed content frame is reported to the renderer (which already has
+// access to it). The mixed content frame is the main frame in this scenario.
+TEST_P(MixedContentCheckerShouldBlockNavigationTest,
+       ReportsAncestorFullUrlToSameProcessRenderer) {
+  const GURL main_frame_url("https://source.com/private/path?token=value#frag");
+  NavigateAndCommit(main_frame_url);
+  TestRenderFrameHost* main_rfh = main_test_rfh();
+  main_rfh->DidEnforceInsecureRequestPolicy(
+      blink::mojom::InsecureRequestPolicy::kLeaveInsecureRequestsAlone);
+
+  TestRenderFrameHost* subframe = static_cast<TestRenderFrameHost*>(
+      NavigationSimulator::NavigateAndCommitFromDocument(
+          GURL("https://source.com/subframe"),
+          main_rfh->AppendChild("subframe")));
+  ASSERT_EQ(subframe->GetProcess(), main_rfh->GetProcess());
+  auto interceptor = std::make_unique<LocalFrameInterceptor>(subframe);
+
+  std::unique_ptr<NavigationSimulatorImpl> navigation =
+      NavigationSimulatorImpl::CreateRendererInitiated(
+          GURL("http://target.com"), subframe);
+  navigation->SetReferrer(blink::mojom::Referrer::New(
+      subframe->GetLastCommittedURL(),
+      network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin));
+  navigation->set_request_context_type(
+      blink::mojom::RequestContextType::INTERNAL);
+  navigation->set_mixed_content_context_type(
+      blink::mojom::MixedContentContextType::kBlockable);
+  navigation->Start();
+
+  auto checker = MixedContentChecker();
+  EXPECT_TRUE(checker.ShouldBlockNavigation(*navigation->GetNavigationHandle(),
+                                            for_redirect()));
+  interceptor->FlushLocalFrameMessages();
+  EXPECT_THAT(interceptor->mixed_content_result(),
+              Optional(FieldsAre(main_frame_url, GURL("http://target.com"),
+                                 /*was_allowed=*/false, for_redirect())));
+}
+
 // Tests to cover MixedContentContextType = kBlockable.
 class MixedContentCheckerShouldBlockNavigationWithBlockableContextTest
     : public MixedContentCheckerShouldBlockNavigationTestBase {
Loading diff…

Original Bug Report

reported by [email protected]

Information Disclosure: Leak of cross-origin ancestor full URL to subframe 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 without the security team.

Overview: The Chrome browser process potentially leaks the full URL of a cross-origin ancestor frame to a subframe’s renderer process. This disclosure occurs via the blink.mojom.LocalFrame.MixedContentFound IPC during mixed content checks. This could violate Site Isolation principles, which restrict access to cross-origin ancestor information to origins only.

Affected files:

  • content/browser/renderer_host/mixed_content_checker.cc

Estimated timestamp from git blame: 2023-11-07

Description

A potential security vulnerability in Chrome’s mixed content checking logic allows a compromised or malicious renderer process to obtain the full URL of a cross-origin ancestor frame.

When a subframe initiates a navigation to an insecure (HTTP) URL, the browser’s MixedContentNavigationThrottle performs security checks. If the subframe is embedded within a secure (HTTPS) context, the mixed content checker identifies the ancestor frame responsible for the secure context (the ‘mixed content frame’).

During this process, the browser calls MixedContentChecker::UpdateRendererOnMixedContentFound in content/browser/renderer_host/mixed_content_checker.cc. This function sends the blink.mojom.LocalFrame.MixedContentFound IPC to the renderer process currently hosting the subframe. Critically, the main_resource_url parameter of this IPC is populated using mixed_content_frame->GetLastCommittedURL(), which returns the full URL of the ancestor frame.

Under Site Isolation, a renderer process should not have access to the full URLs of cross-origin frames in other processes. The web platform only exposes the origins of such ancestors (e.g., via location.ancestorOrigins). Leaking the full URL can expose sensitive information such as OAuth authorization codes, session tokens, CSRF tokens, or private document IDs contained within the URL path, query string, or fragment.

Technical Analysis

The potential vulnerability exists in the following call chain:

  1. MixedContentNavigationThrottle::WillStartRequest calls MixedContentChecker::ShouldBlockNavigation.
  2. MixedContentChecker::InWhichFrameIsContentMixed identifies the ancestor frame (often the outermost main frame) as the source of the mixed content restriction.
  3. MixedContentChecker::ShouldBlockInternal is called and unconditionally sets should_report_to_renderer = true.
  4. MixedContentChecker::ShouldBlockNavigation then calls UpdateRendererOnMixedContentFound.
  5. UpdateRendererOnMixedContentFound retrieves the current RenderFrameHost for the navigating subframe: rfh = navigation_request->frame_tree_node()->current_frame_host().
  6. It then sends the MixedContentFound IPC to this rfh, providing the full GURL from the ancestor frame: mixed_content_frame->GetLastCommittedURL().

Because the IPC is sent to the current_frame_host of the subframe before the navigation commits, it is delivered to the process currently controlled by the attacker. This leak occurs even if the navigation is ultimately blocked by the mixed content policy.

Impact

This is a potential Site Isolation bypass resulting in cross-origin information disclosure. An attacker-controlled subframe can learn the full URL of its parent or the top-level frame, even if they are cross-origin. This can lead to the theft of sensitive data passed in URLs.

Suggested Steps to Reproduce

(Note: These are suggested steps to reproduce; our tooling agent does not have the ability to run code to confirm this exploit)

  1. Use a Chrome build with Site Isolation enabled (default).
  2. Navigate the top-level frame to a sensitive URL: https://victim.example/dashboard?session_token=secret_123#private_data.
  3. The victim page embeds an attacker-controlled iframe: <iframe src="https://attacker.example/"></iframe>.
  4. From the attacker iframe, initiate a navigation to any HTTP URL (e.g., by creating a nested iframe with src="http://example.com/").
  5. A compromised attacker renderer can intercept the blink.mojom.LocalFrame::MixedContentFound Mojo message sent by the browser.
  6. The main_resource_url parameter in that message will contain the full URL https://victim.example/dashboard?session_token=secret_123#private_data.

Suggested Fix

To remediate this issue, the browser should not send the full URL of the ancestor frame to the renderer process. Instead, it should strip the path, query, and fragment components from the URL, or just send the origin of the ancestor frame. The mixed_content_frame->GetLastCommittedOrigin().GetURL() could be used instead of mixed_content_frame->GetLastCommittedURL() when populating the IPC argument.

Evaluated with Chrome root at commit: e9e0fcbb690b1a8c1a26c81c2a9ea23d6e178368


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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