Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in SSL
DescriptionInsufficient validation of untrusted input in SSL
ComponentSSL
Bug ClassLogic Error
Tracker484547631
Fix commit8afff2500635 (chromium/src) +90/-43
CISA KEVNot listed
Creditedheesun
Disclosed2026-05-05

Changed Functions

FunctionChangeNotes
if
content/browser/loader/content_security_notifier.cc
modified
if
content/browser/renderer_host/mixed_content_checker.cc
modified

Files Changed

  • content/browser/loader/content_security_notifier.cc
  • content/browser/loader/content_security_notifier.h
  • content/browser/renderer_host/mixed_content_checker.cc
  • content/browser/renderer_host/mixed_content_checker.h
  • content/browser/renderer_host/render_frame_host_impl.cc
  • content/browser/renderer_host/render_frame_host_impl.h
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"
Loading diff…

Original Bug Report

reported by [email protected]

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:

  1. ContentSecurityNotifier::NotifyInsecureContentRan(origin, url) - no check
  2. RenderFrameHostImpl::OnDidRunInsecureContent(origin, url) - no check
  3. SSLManager::DidRunMixedContent(origin)
  4. SSLHostStateDelegate::HostRanInsecureContent(host, MIXED_CONTENT) - profile-level storage
  5. SSLManager::NotifySSLInternalStateChanged() - iterates ALL SSLManagers in BrowserContext
  6. 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:

  1. Extract the attached PoC files into a directory

  2. Generate self-signed TLS certificates: openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem
    -days 365 -nodes -subj ‘/CN=localhost’

  3. 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.

  4. Start the PoC server: python3 server.py Server starts on https://127.0.0.1:8443/

  5. 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

View on issue tracker