Medium watch CVSS 3.1 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Core
DescriptionInformation leak in Core
ComponentCore
Sub-componentMixedContent
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker498327743
Fix commitcb63f7f190a0 (chromium/src) +99/-4
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-31 (per vendor advisory)
Fix landed2026-07-21 (public git)
Disclosed2026-08-25
Patch gap35 days — fix public in source before the advisory

Background

Mixed content check for navigations
the browser-side MixedContentChecker (content/browser/renderer_host/mixed_content_checker.cc) detects when a frame nested under a secure (HTTPS) ancestor navigates to an insecure (HTTP) URL and decides whether to block it.
`MixedContentFound` IPC
a message sent to the navigating frame’s renderer via GetAssociatedLocalFrame() carrying the “main resource URL” of the secure frame that makes the load mixed, the insecure target URL, and whether it was allowed, so Blink can log a console warning.
Site isolation / `SiteInstanceGroup`
cross-site frames are hosted in separate renderer processes, and a renderer is expected to hold only the origin, not the full URL (path, query, fragment), of cross-process frames.
`blink::MainResourceUrlForFrame()`
the renderer-side helper that already reports only the origin when the mixed content frame is a remote (out-of-process) frame; the browser path is supposed to mirror it.

Root Cause Analysis

Before the patch, the caller in mixed_content_checker.cc passed mixed_content_frame->GetLastCommittedURL() straight into UpdateRendererOnMixedContentFound(), which forwarded it in MixedContentFound to the navigating frame’s renderer regardless of which process hosted that renderer. When a cross-site subframe (in its own process under site isolation) initiated an insecure navigation, the browser therefore sent it the full committed URL of the secure ancestor, including private path, query and fragment (the test uses https://source.com/private/path?token=value#frag). This violates the site-isolation invariant that a renderer only learns the origin of cross-process frames, and diverges from Blink’s MainResourceUrlForFrame().

The fix changes UpdateRendererOnMixedContentFound() to take the RenderFrameHostImpl* itself and compute the URL: the full GetLastCommittedURL() only when the mixed content frame and the navigating frame share a SiteInstanceGroup, otherwise GetLastCommittedOrigin().GetURL().

Key insight
The browser forwarded a privileged datum (an ancestor frame’s full URL) to a renderer without checking whether that renderer was entitled to it. The fix gates the full URL on same-SiteInstanceGroup hosting and degrades it to the origin across process boundaries.

Attack Path

  1. Cross-site frame under a secure page An attacker’s page is embedded as a cross-site iframe inside an HTTPS page whose URL contains sensitive path/query/fragment data, with mixed content not upgraded (the test sets kLeaveInsecureRequestsAlone).
  2. Subframe starts an insecure navigation The attacker-controlled subframe initiates a navigation to an http:// URL, which the browser’s MixedContentChecker flags as mixed content.
  3. Browser sends ancestor's full URL UpdateRendererOnMixedContentFound() sends MixedContentFound to the subframe’s renderer with the ancestor’s full GetLastCommittedURL() rather than its origin.
  4. Attacker-process reads the URL The data arrives in the attacker site’s renderer process; page script would not normally read it from a console message, so extracting it most plausibly requires a compromised renderer or some other observer of that message (inferred).

Impact Assessment

A cross-site renderer process learns the full URL of a secure ancestor frame (path, query string and fragment, which may contain tokens) that site isolation is meant to withhold, beyond what the default referrer policy would expose. The advisory classifies this as “Information leak in Core” and reports no in-the-wild exploitation; practical exploitation from ordinary web content without a compromised renderer is not demonstrated by the patch (inferred), and it requires the attacker to be framed by the victim page and to trigger an insecure navigation that is reported to the renderer.

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

Audit Directions

  • Browser-to-renderer messages carrying other frames' URLs
    Audit LocalFrame/RemoteFrame Mojo calls that include an ancestor’s, opener’s or embedder’s URL (console messages, CSP/violation reports, DevTools-ish diagnostics) and confirm they reduce to origin when the recipient is in a different SiteInstanceGroup.
  • Browser/renderer logic parity
    Where browser code reimplements a Blink helper (here MainResourceUrlForFrame()), check that cross-process privacy handling in the renderer version was also carried over.
  • Diagnostic-only data paths
    Messages meant only for console logging often skip privacy review; treat any full URL, header or cookie-adjacent data sent “just for the console” to a possibly cross-site renderer as a potential leak.
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