CVE-2026-7996
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcontent/browser/loader/content_security_notifier.cc |
modified | |
ifcontent/browser/renderer_host/mixed_content_checker.cc |
modified |
Files Changed
content/browser/loader/content_security_notifier.cccontent/browser/loader/content_security_notifier.hcontent/browser/renderer_host/mixed_content_checker.cccontent/browser/renderer_host/mixed_content_checker.hcontent/browser/renderer_host/render_frame_host_impl.cccontent/browser/renderer_host/render_frame_host_impl.h
Patch
From 8afff25006358da9e15daa73b4eeda9752c9e11e Mon Sep 17 00:00:00 2001 From: Mike West <[email protected]> Date: Fri, 20 Mar 2026 07:19:23 -0700 Subject: [PATCH] Calculate `NotifyInsecureContentRan` origin in the browser. Bug: 484547631 Change-Id: I5c6f817bcb061ac3ce1ed519800c491e441a7722 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7679211 Reviewed-by: Hiroki Nakagawa <[email protected]> Reviewed-by: Arthur Sonzogni <[email protected]> Commit-Queue: Mike West <[email protected]> Cr-Commit-Position: refs/heads/main@{#1602566} --- diff --git a/content/browser/loader/content_security_notifier.cc b/content/browser/loader/content_security_notifier.cc index 1c30ab73..938174ec 100644 --- a/content/browser/loader/content_security_notifier.cc +++ b/content/browser/loader/content_security_notifier.cc @@ -27,11 +27,11 @@ } void ContentSecurityNotifier::NotifyInsecureContentRan( - const GURL& origin, - const GURL& insecure_url) { + const GURL& insecure_url, + blink::mojom::ContentSecurityNotifier::InsecureContentOrigin origin_type) { auto* render_frame_host = RenderFrameHostImpl::FromID(render_frame_host_id_); if (render_frame_host) { - render_frame_host->OnDidRunInsecureContent(origin, insecure_url); + render_frame_host->OnDidRunInsecureContent(insecure_url, origin_type); } } diff --git a/content/browser/loader/content_security_notifier.h b/content/browser/loader/content_security_notifier.h index 0417eec..a41ea0c 100644 --- a/content/browser/loader/content_security_notifier.h +++ b/content/browser/loader/content_security_notifier.h @@ -26,8 +26,10 @@ // blink::mojom::ContentSecurityNotifier implementation. void NotifyContentWithCertificateErrorsRan() override; void NotifyContentWithCertificateErrorsDisplayed() override; - void NotifyInsecureContentRan(const GURL& origin, - const GURL& insecure_url) override; + void NotifyInsecureContentRan( + const GURL& insecure_url, + blink::mojom::ContentSecurityNotifier::InsecureContentOrigin origin_type) + override; private: const GlobalRenderFrameHostId render_frame_host_id_; diff --git a/content/browser/renderer_host/mixed_content_checker.cc b/content/browser/renderer_host/mixed_content_checker.cc index f5f14eaa..233ddd4 100644 --- a/content/browser/renderer_host/mixed_content_checker.cc +++ b/content/browser/renderer_host/mixed_content_checker.cc @@ -61,11 +61,6 @@ url::kHttpsScheme; } -// This mirrors `blink::MixedContentChecker::IsMixedContent()`. -bool IsMixedContent(const url::Origin& origin, const GURL& url) { - return !IsUrlPotentiallySecure(url) && - DoesOriginSchemeRestrictMixedContent(origin); -} // This mirrors `blink::MixedContentChecker::InWhichFrameIsContentMixed()` but // without reporting to renderer. @@ -88,11 +83,13 @@ // Check the main frame first. RenderFrameHostImpl* main_frame = initiator_frame->GetOutermostMainFrame(); - if (IsMixedContent(main_frame->GetLastCommittedOrigin(), url)) { + if (MixedContentChecker::IsMixedContent(main_frame->GetLastCommittedOrigin(), + url)) { return main_frame; } - if (IsMixedContent(initiator_frame->GetLastCommittedOrigin(), url)) { + if (MixedContentChecker::IsMixedContent( + initiator_frame->GetLastCommittedOrigin(), url)) { return initiator_frame; } @@ -181,6 +178,13 @@ } // namespace +// static +bool MixedContentChecker::IsMixedContent(const url::Origin& security_origin, + const GURL& target_url) { + return !IsUrlPotentiallySecure(target_url) && + DoesOriginSchemeRestrictMixedContent(security_origin); +} + MixedContentChecker::MixedContentChecker() = default; MixedContentChecker::~MixedContentChecker() = default; @@ -302,9 +306,9 @@ prefs.allow_running_insecure_content, mixed_content_frame->GetLastCommittedOrigin(), url); if (allowed) { - const GURL& origin_url = - mixed_content_frame->GetLastCommittedOrigin().GetURL(); - mixed_content_frame->OnDidRunInsecureContent(origin_url, url); + mixed_content_frame->OnDidRunInsecureContent( + url, blink::mojom::ContentSecurityNotifier::InsecureContentOrigin:: + kCurrentFrame); if (mixed_content_features) { mixed_content_features->insert( blink::mojom::WebFeature::kMixedContentBlockableAllowed); @@ -401,7 +405,7 @@ } // Note: The code below should behave the same way as the two calls to - // `MeasureStricterVersionOfIsMixedContent()` from inside + // `MeasureStricterVersionOfMixedContentChecker::IsMixedContent()` from inside // `blink::MixedContentChecker::InWhichFrameIsContentMixed()`. if (mixed_content_frame) { // We're currently only checking for mixed content in `https://*` contexts. @@ -456,7 +460,7 @@ bool MixedContentChecker::IsMixedContentForTesting(const GURL& origin_url, const GURL& url) { const url::Origin origin = url::Origin::Create(origin_url); - return IsMixedContent(origin, url); + return MixedContentChecker::IsMixedContent(origin, url); } } // namespace content diff --git a/content/browser/renderer_host/mixed_content_checker.h b/content/browser/renderer_host/mixed_content_checker.h index bc15ef7..3d9a1783 100644 --- a/content/browser/renderer_host/mixed_content_checker.h +++ b/content/browser/renderer_host/mixed_content_checker.h @@ -43,6 +43,9 @@ bool ShouldBlockNavigation(NavigationHandle& navigation_handle, bool for_redirect); + // Returns whether `url` is mixed content with respect to `origin`. + static bool IsMixedContent(const url::Origin& origin, const GURL& url); + // Checks if a fetch keepalive request that loads `url` should be blocked or // not due to mixed content, without reporting back to renderer. // diff --git a/content/browser/renderer_host/render_frame_host_impl.cc b/content/browser/renderer_host/render_frame_host_impl.cc index 1c9f0c6..85353aa 100644 --- a/content/browser/renderer_host/render_frame_host_impl.cc +++ b/content/browser/renderer_host/render_frame_host_impl.cc @@ -139,6 +139,7 @@ #include "content/browser/renderer_host/ipc_utils.h" #include "content/browser/renderer_host/local_network_access_util.h" #include "content/browser/renderer_host/media/peer_connection_tracker_host.h" +#include "content/browser/renderer_host/mixed_content_checker.h" #include "content/browser/renderer_host/navigation_controller_impl.h" #include "content/browser/renderer_host/navigation_entry_impl.h" #include "content/browser/renderer_host/navigation_metrics_utils.h" @@ -19222,18 +19223,33 @@ SetPolicyContainerHost(std::move(policy_container_host)); } -void RenderFrameHostImpl::OnDidRunInsecureContent(const GURL& security_origin, - const GURL& target_url) { +void RenderFrameHostImpl::OnDidRunInsecureContent( + const GURL& target_url, + blink::mojom::ContentSecurityNotifier::InsecureContentOrigin origin_type) { + url::Origin security_origin = + (origin_type == + blink::mojom::ContentSecurityNotifier::InsecureContentOrigin::kTopFrame) + ? GetOutermostMainFrame()->GetLastCommittedOrigin() + : GetLastCommittedOrigin(); + + if (!MixedContentChecker::IsMixedContent(security_origin, target_url)) { + mojo::ReportBadMessage( + "NotifyInsecureContentRan called for non-mixed content."); + return; + } + + const GURL& security_origin_url = security_origin.GetURL(); OPTIONAL_TRACE_EVENT2("content", "RenderFrameHostImpl::DidRunInsecureContent", - "security_origin", security_origin, "target_url", + "security_origin", security_origin_url, "target_url", target_url); RecordAction(base::UserMetricsAction("SSL.RanInsecureContent")); - if (base::EndsWith(security_origin.spec(), kDotGoogleDotCom, + if (base::EndsWith(security_origin_url.spec(), kDotGoogleDotCom, base::CompareCase::INSENSITIVE_ASCII)) { RecordAction(base::UserMetricsAction("SSL.RanInsecureContentGoogle")); } - frame_tree_->controller().ssl_manager()->DidRunMixedContent(security_origin); + frame_tree_->controller().ssl_manager()->DidRunMixedContent( + security_origin_url); } void RenderFrameHostImpl::OnDidRunContentWithCertificateErrors() { diff --git a/content/browser/renderer_host/render_frame_host_impl.h b/content/browser/renderer_host/render_frame_host_impl.h index f08b086..0eac0e40 100644 --- a/content/browser/renderer_host/render_frame_host_impl.h +++ b/content/browser/renderer_host/render_frame_host_impl.h @@ -157,6 +157,7 @@ #include "third_party/blink/public/mojom/image_downloader/image_downloader.mojom.h" #include "third_party/blink/public/mojom/input/focus_type.mojom-forward.h" #include "third_party/blink/public/mojom/installedapp/installed_app_provider.mojom-forward.h"
Original Bug Report
Security: ContentSecurityNotifier::NotifyInsecureContentRan accepts arbitrary origin from renderer, enabling cross-origin SSL state pollution
Security Bug
Important: Please do not change the component of this bug manually.
Please READ THIS FAQ before filing a bug: https://chromium.googlesource.com/chromium/src/+/HEAD/docs/security/faq.md
Please see the following link for instructions on filing security bugs: https://www.chromium.org/Home/chromium-security/reporting-security-bugs
Reports may be eligible for reward payments under the Chrome VRP: https://g.co/chrome/vrp
NOTE: Security bugs are normally made public once a fix has been widely deployed.
VULNERABILITY DETAILS
ContentSecurityNotifier::NotifyInsecureContentRan() accepts a renderer-supplied
origin parameter via Mojo IPC without validating it against the renderer’s
committed origin. A compromised renderer can spoof any origin, causing the browser
process to mark arbitrary HTTPS domains as having run insecure (mixed) content.
This pollutes the profile-level SSL state (SSLHostStateDelegate), degrading security indicators for the targeted domain across ALL tabs in the browser profile. The poisoned state persists until browser restart.
This violates the site isolation guarantee: a compromised renderer for attacker.com should not be able to affect the security presentation of bank.com.
Root Cause: In content/browser/loader/content_security_notifier.cc:
void ContentSecurityNotifier::NotifyInsecureContentRan( const GURL& origin, const GURL& insecure_url) { auto* render_frame_host = RenderFrameHostImpl::FromID(render_frame_host_id_); if (render_frame_host) { render_frame_host->OnDidRunInsecureContent(origin, insecure_url); // No validation that origin matches render_frame_host->GetLastCommittedOrigin() } }
The mojom definition already has a TODO acknowledging this design flaw (content_security_notifier.mojom line 26-27):
// TODO(nhiroki): Stop passing the origin, and instead take it from the // execution context host.
The spoofed origin flows through the following chain without validation:
- ContentSecurityNotifier::NotifyInsecureContentRan(origin, url) - no check
- RenderFrameHostImpl::OnDidRunInsecureContent(origin, url) - no check
- SSLManager::DidRunMixedContent(origin)
- SSLHostStateDelegate::HostRanInsecureContent(host, MIXED_CONTENT) - profile-level storage
- SSLManager::NotifySSLInternalStateChanged() - iterates ALL SSLManagers in BrowserContext
- Each tab’s UpdateEntry() sets SSLStatus::RAN_INSECURE_CONTENT on matching hosts
Consequences:
- Renderer for attacker.com degrades bank.com’s indicator to “broken HTTPS”
- Persists until browser restart (profile-level, in-memory std::set)
- All tabs visiting target domain are affected
- No user interaction required from the compromised renderer
VERSION
Chrome Version: 147.0.7682.0 (dev) — affects all versions since ContentSecurityNotifier was introduced (2020, Copyright header in the source file) Operating System: All platforms (verified on macOS 15.3.1, Darwin 25.2.0)
REPRODUCTION CASE
Prerequisites:
- Any Chromium build (does not require ASAN)
- –enable-blink-features=MojoJS flag (simulates compromised renderer)
Setup:
-
Extract the attached PoC files into a directory
-
Generate self-signed TLS certificates: openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem
-days 365 -nodes -subj ‘/CN=localhost’ -
The PoC server needs access to Chromium build output’s gen/ directory for MojoJS bindings. Place files at the same level as your chromium/src/ dir, or adjust the CHROMIUM_SRC path in server.py.
-
Start the PoC server: python3 server.py Server starts on https://127.0.0.1:8443/
-
Launch Chromium with MojoJS enabled: ./run.sh chrome Or manually: chrome –enable-blink-features=MojoJS
–ignore-certificate-errors
–no-first-run
–user-data-dir=/tmp/csn-poc
https://127.0.0.1:8443/
Steps: 6. On the PoC page, enter a target domain (e.g., www.google.com) 7. Click “Poison SSL State” 8. Open a NEW TAB and navigate to https://www.google.com 9. Open DevTools (F12) -> Security tab
Expected result: Security panel shows “This page is secure” — Google serves all content over HTTPS.
Actual result: Security panel shows: “This page is not secure (broken HTTPS).” “Resources - active mixed content” Google.com did NOT actually load any mixed content. The degraded state was caused entirely by the spoofed Mojo IPC from 127.0.0.1.
How the PoC works: The PoC uses –enable-blink-features=MojoJS to simulate a compromised renderer. It loads generated MojoJS bindings (content_security_notifier.mojom.m.js) and calls:
const remote = ContentSecurityNotifier.getRemote();
remote.notifyInsecureContentRan(
{ url: "https://www.google.com/" }, // spoofed origin
{ url: "http://evil.example.com/script.js" } // fake insecure URL
);
In a real attack, a compromised renderer would send this Mojo message directly without needing the MojoJS flag.
Attached files:
- poc.html : MojoJS exploit page (loads generated mojom bindings)
- server.py : HTTPS server (serves PoC page + gen/ directory for MojoJS modules)
- run.sh : Launch script for Chromium with correct flags
SUGGESTED FIX
Option 1 — Validate origin against committed origin:
void ContentSecurityNotifier::NotifyInsecureContentRan( const GURL& origin, const GURL& insecure_url) { auto* render_frame_host = RenderFrameHostImpl::FromID(render_frame_host_id_); if (!render_frame_host) return; if (!render_frame_host->GetLastCommittedOrigin().IsSameOriginWith( url::Origin::Create(origin))) { mojo::ReportBadMessage( “ContentSecurityNotifier: origin does not match committed origin”); return; } render_frame_host->OnDidRunInsecureContent(origin, insecure_url); }
Option 2 — Remove origin parameter entirely (resolves existing TODO):
// content_security_notifier.mojom: NotifyInsecureContentRan(url.mojom.Url insecure_url);
// content_security_notifier.cc: void ContentSecurityNotifier::NotifyInsecureContentRan( const GURL& insecure_url) { auto* render_frame_host = RenderFrameHostImpl::FromID(render_frame_host_id_); if (!render_frame_host) return; GURL origin = render_frame_host->GetLastCommittedOrigin().GetURL(); render_frame_host->OnDidRunInsecureContent(origin, insecure_url); }
Option 2 addresses the TODO(nhiroki) and eliminates the trust-the-renderer pattern. Callers in mixed_content_checker.cc would need updating to stop passing origin.
CREDIT INFORMATION
Reporter credit: heesun