CVE-2026-78941
Overview
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().
SiteInstanceGroup hosting and degrades it to the origin across process boundaries.Attack Path
- 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). - Subframe starts an insecure navigation
The attacker-controlled subframe initiates a navigation to an
http://URL, which the browser’sMixedContentCheckerflags as mixed content. - Browser sends ancestor's full URL
UpdateRendererOnMixedContentFound()sendsMixedContentFoundto the subframe’s renderer with the ancestor’s fullGetLastCommittedURL()rather than its origin. - 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
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/renderer_host/mixed_content_checker.cc |
modified | |
MixedContentCheckerShouldBlockNavigationWithBlockableContextTestcontent/browser/renderer_host/mixed_content_checker_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/mixed_content_checker.cccontent/browser/renderer_host/mixed_content_checker_unittest.cc
Audit Directions
- Browser-to-renderer messages carrying other frames' URLsAudit
LocalFrame/RemoteFrameMojo 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 differentSiteInstanceGroup. - Browser/renderer logic parityWhere 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 pathsMessages 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.
Patch
From cb63f7f190a035064508206f10a23e869f835095 Mon Sep 17 00:00:00 2001
From: Andrew Paseltiner <apaseltiner@chromium.org>
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 <alexmos@chromium.org>
Commit-Queue: Andrew Paseltiner <apaseltiner@chromium.org>
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 {
Regression Test / PoC
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 {
Original Bug Report
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:
MixedContentNavigationThrottle::WillStartRequestcallsMixedContentChecker::ShouldBlockNavigation.MixedContentChecker::InWhichFrameIsContentMixedidentifies the ancestor frame (often the outermost main frame) as the source of the mixed content restriction.MixedContentChecker::ShouldBlockInternalis called and unconditionally setsshould_report_to_renderer = true.MixedContentChecker::ShouldBlockNavigationthen callsUpdateRendererOnMixedContentFound.UpdateRendererOnMixedContentFoundretrieves the currentRenderFrameHostfor the navigating subframe:rfh = navigation_request->frame_tree_node()->current_frame_host().- It then sends the
MixedContentFoundIPC to thisrfh, providing the fullGURLfrom 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)
- Use a Chrome build with Site Isolation enabled (default).
- Navigate the top-level frame to a sensitive URL:
https://victim.example/dashboard?session_token=secret_123#private_data. - The victim page embeds an attacker-controlled iframe:
<iframe src="https://attacker.example/"></iframe>. - From the attacker iframe, initiate a navigation to any HTTP URL (e.g., by creating a nested iframe with
src="http://example.com/"). - A compromised attacker renderer can intercept the
blink.mojom.LocalFrame::MixedContentFoundMojo message sent by the browser. - The
main_resource_urlparameter in that message will contain the full URLhttps://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.