Low watch CVSS 4.2 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
4.2
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in SSL
DescriptionInsufficient validation of untrusted input in SSL
ComponentSSL
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
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.
Tracker484547631
Fix commit8afff2500635 (chromium/src) +90/-43
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
Creditedheesun
Reported2026-02-15 (per vendor advisory)
Fix landed2026-03-20 (public git)
Disclosed2026-05-05
Patch gap46 days — fix public in source before the advisory

Background

Mixed content
A secure (https) document loading or running a non-secure (http) subresource, which Chrome tracks to downgrade the page’s security state.
`ContentSecurityNotifier`
A browser-side implementation of a blink::mojom interface that receives insecure-content notifications sent from the renderer.
`DidRunMixedContent`
An ssl_manager() call that records, against a specific origin, that active mixed content ran, which drives the SSL/security UI state for that origin.
`ReportBadMessage`
A Mojo mechanism the browser uses to reject and flag an IPC whose contents violate an expected invariant.

Root Cause Analysis

The renderer-facing method ContentSecurityNotifier::NotifyInsecureContentRan previously accepted a const GURL& origin directly from the renderer and forwarded it, via RenderFrameHostImpl::OnDidRunInsecureContent, straight into ssl_manager()->DidRunMixedContent(security_origin) without the browser re-deriving or validating that origin. A compromised renderer could therefore supply an arbitrary origin string, causing the browser to attribute a “ran insecure content” security-state change to an origin of the attacker’s choosing, and to do so even when the reported load was not actually mixed content.

The fix removes the renderer-supplied origin entirely: the method now takes insecure_url plus an InsecureContentOrigin origin_type enum, and the browser itself computes security_origin from trusted frame state (GetOutermostMainFrame()->GetLastCommittedOrigin() for kTopFrame, else GetLastCommittedOrigin()). It then calls the newly exposed MixedContentChecker::IsMixedContent(security_origin, target_url) and, if the pair is not genuinely mixed content, invokes ReportBadMessage and returns before touching the SSL manager. This restores the invariant that the security origin is always browser-authoritative and that the notification is only honored for real mixed content.

Key insight
The browser trusted a renderer-provided origin GURL as the subject of a security-state mutation; the fix makes the browser derive the origin from its own frame tree and validates that the reported content is actually mixed content before acting.

Attack Path

  1. Compromise a renderer An attacker obtains arbitrary-message capability in a renderer process (the standard precondition for a browser-side IPC-validation fix).
  2. Forge the notification The renderer sends NotifyInsecureContentRan with an attacker-chosen origin GURL and an insecure_url that need not correspond to any real mixed-content load.
  3. Browser trusts the origin Pre-patch, OnDidRunInsecureContent forwards the supplied origin to ssl_manager()->DidRunMixedContent, mutating the tracked SSL/security state for an origin the attacker picked.
  4. Security-state confusion The result is a spoofed or misattributed mixed-content security state in the browser’s SSL tracking for an origin the renderer should not control (full consequence beyond state misattribution is not demonstrated by the diff) (inferred).

Impact Assessment

A compromised renderer could cause the browser process to record a mixed-content security-state change against an arbitrary origin, and to do so without the load being genuine mixed content, corrupting the integrity of SSL/security UI state. The precondition is a compromised or malicious renderer able to send the Mojo message; the vendor advisory rates this low severity and describes it as insufficient validation of untrusted input in SSL. No exploitation-in-the-wild is stated by the advisory.

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

Audit Directions

  • Renderer-supplied principals
    Audit any browser-side IPC that accepts an origin, URL, or security principal from the renderer and uses it to drive security state; the browser should re-derive such principals from its own frame tree.
  • Mirror-logic drift
    Where browser code reimplements a Blink security check (here mirroring blink::MixedContentChecker::IsMixedContent), verify the browser copy is actually invoked on the privileged path, not just declared.
  • State-mutating notifications
    Review blink::mojom notifier interfaces whose handlers mutate SSL/security UI state, ensuring each validates the asserted condition before applying the change rather than trusting the renderer’s assertion.
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