Low watch CVSS 5 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Cast
DescriptionInappropriate implementation in Cast
ComponentCast
Sub-componentMedia router
Bug ClassLogic Error
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.
Tracker496555077
Fix commit52c5e880feae (chromium/src) +41/-9
CWECWE-693
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-03-26 (per vendor advisory)
Fix landed2026-04-01 (public git)
Disclosed2026-05-05
Patch gap34 days — fix public in source before the advisory

Background

Presentation/Cast URL validation
media_router::IsValidPresentationUrl and media_router::IsValidStandardPresentationSource decide which renderer-supplied URLs Media Router will act on, gating scheme (and now host).
`IsSchemeAllowed`
a helper used during URL validation that previously accepted any HTTP or HTTPS URL plus a fixed list of special schemes (kAllowedSchemes).
Private Network Access (PNA)
a security mechanism that restricts public web content from reaching private/local network endpoints without extra checks.
`net::IsLocalhost`
a utility returning true only for localhost / loopback (e.g. 127.0.0.1) hosts.

Root Cause Analysis

IsSchemeAllowed and IsValidStandardPresentationSource accepted any http URL via SchemeIsHTTPOrHTTPS(), so a compromised renderer could point a Presentation API request at an arbitrary local http:// endpoint. Because these presentation requests are issued from the browser process (not subject to the renderer’s PNA enforcement), permitting arbitrary http hosts let the renderer reach private-network resources it should not — violating the invariant that cleartext-http presentation targets must be local.

The fix narrows the http branch: HTTPS is still broadly allowed, but http is accepted only when net::IsLocalhost(url) is true, and all other schemes still fall through to the kAllowedSchemes any_of check. IsValidStandardPresentationSource is restructured the same way — valid HTTPS (excluding the legacy Cast prefix), http only if localhost, else reject. The regression tests confirm http://127.0.0.1 and http://localhost pass while http://google.com is rejected. This is a tightening of previously over-broad scheme acceptance.

Key insight
The validators allowed any http host, enabling a browser-process-initiated request to bypass renderer-side PNA checks; the fix restricts http to localhost/loopback via net::IsLocalhost while leaving HTTPS and the special schemes intact.

Attack Path

  1. Compromise a renderer the attacker runs code in a renderer able to issue Presentation API / Media Router requests.
  2. Supply an arbitrary local `http://` URL provide a presentation URL such as http://<private-network-host> that previously passed IsSchemeAllowed / IsValidStandardPresentationSource solely because the scheme was http.
  3. Issue the request from the browser process Media Router processes the URL in the browser process, where the renderer-side PNA restriction does not apply, so the request reaches the local/private endpoint.
  4. Bypass PNA pre-patch this let the renderer probe or interact with private-network HTTP resources; post-patch, non-localhost http is rejected by net::IsLocalhost, blocking the bypass.

Impact Assessment

A compromised renderer could bypass Private Network Access protections by initiating a Presentation API request to an arbitrary local HTTP URL handled in the browser process; the precondition is renderer compromise (or the ability to drive these APIs). The advisory rates this low severity and classifies it as inappropriate implementation in Cast. No in-the-wild exploitation is indicated; the scope is limited to reaching/probing local HTTP endpoints, with any further network impact not demonstrated by the diff.

Changed Functions

FunctionChangeNotes
TEST
components/media_router/common/media_source_unittest.cc
modified

Files Changed

  • components/media_router/common/media_source.cc
  • components/media_router/common/media_source_unittest.cc

Audit Directions

  • `SchemeIsHTTPOrHTTPS` over-acceptance
    search for validators that accept http and https together when only https (or localhost http) should be trusted, especially for inputs consumed in the browser process.
  • Browser-process requests and PNA
    audit code paths where renderer-supplied URLs are fetched/connected from the browser process, which sidesteps renderer PNA enforcement and needs its own local-only/host checks.
  • Host-independent scheme checks
    flag allowlists that validate scheme without validating host for schemes (like cleartext http) that are only safe against local/loopback targets.
From 52c5e880feae5309309788125965c11c4fc72036 Mon Sep 17 00:00:00 2001
From: mark a. foltz <mfoltz@chromium.org>
Date: Wed, 01 Apr 2026 10:41:50 -0700
Subject: [PATCH] [media router] Restrict http Presentation URLs to localhost.

This change updates media_router::IsValidPresentationUrl and
media_router::IsValidStandardPresentationSource to only allow the
http scheme if the hostname is localhost or a local IP address.
This prevents a compromised renderer from bypassing Private Network
Access (PNA) checks by initiating a Presentation API request to an
arbitrary local HTTP URL.

AI disclosure: Prepared by gemini_cli.

Bug: 496555077
Change-Id: Iae79d31a32d148192195c0f16efde3826ffcfaaf
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7721351
Reviewed-by: Muyao Xu <muyaoxu@google.com>
Commit-Queue: Mark Foltz <mfoltz@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1608642}
---

diff --git a/components/media_router/common/media_source.cc b/components/media_router/common/media_source.cc
index 38e599d..f4d09a73 100644
--- a/components/media_router/common/media_source.cc
+++ b/components/media_router/common/media_source.cc
@@ -23,6 +23,7 @@
 #include "net/base/url_util.h"
 #include "third_party/blink/public/platform/modules/remoteplayback/remote_playback_source.h"
 #include "url/gurl.h"
+#include "url/url_constants.h"
 
 namespace media_router {
 
@@ -49,10 +50,15 @@
      "test"}};
 
 bool IsSchemeAllowed(const GURL& url) {
-  return url.SchemeIsHTTPOrHTTPS() ||
-         std::ranges::any_of(kAllowedSchemes, [&url](const char* const scheme) {
-           return url.SchemeIs(scheme);
-         });
+  if (url.SchemeIs(url::kHttpsScheme)) {
+    return true;
+  } else if (url.SchemeIs(url::kHttpScheme)) {
+    return net::IsLocalhost(url);
+  } else {
+    return std::ranges::any_of(
+        kAllowedSchemes,
+        [&url](const char* const scheme) { return url.SchemeIs(scheme); });
+  }
 }
 
 bool IsSystemAudioCaptureSupported() {
@@ -82,9 +88,17 @@
 
 bool IsValidStandardPresentationSource(const std::string& media_source) {
   const GURL source_url(media_source);
-  return source_url.is_valid() && source_url.SchemeIsHTTPOrHTTPS() &&
-         !base::StartsWith(source_url.spec(), kLegacyCastPresentationUrlPrefix,
-                           base::CompareCase::INSENSITIVE_ASCII);
+  if (!source_url.is_valid()) {
+    return false;
+  } else if (source_url.SchemeIs(url::kHttpsScheme)) {
+    return !base::StartsWith(source_url.spec(),
+                             kLegacyCastPresentationUrlPrefix,
+                             base::CompareCase::INSENSITIVE_ASCII);
+  } else if (source_url.SchemeIs(url::kHttpScheme)) {
+    return net::IsLocalhost(source_url);
+  } else {
+    return false;
+  }
 }
 
 bool IsAutoJoinPresentationId(const std::string& presentation_id) {
diff --git a/components/media_router/common/media_source_unittest.cc b/components/media_router/common/media_source_unittest.cc
index 8595e87..5340b7b 100644
--- a/components/media_router/common/media_source_unittest.cc
+++ b/components/media_router/common/media_source_unittest.cc
@@ -33,6 +33,24 @@
   EXPECT_TRUE(IsValidPresentationUrl(GURL("cast://foo")));
   EXPECT_TRUE(IsValidPresentationUrl(GURL("cast:foo")));
   EXPECT_TRUE(IsValidPresentationUrl(GURL("remote-playback:foo")));
+
+  EXPECT_TRUE(IsValidPresentationUrl(GURL("http://127.0.0.1")));
+  EXPECT_TRUE(IsValidPresentationUrl(GURL("http://localhost")));
+  EXPECT_FALSE(IsValidPresentationUrl(GURL("http://google.com")));
+}
+
+TEST(MediaSourceTest, IsValidStandardPresentationSource) {
+  EXPECT_FALSE(IsValidStandardPresentationSource(""));
+  EXPECT_FALSE(IsValidStandardPresentationSource("unsupported-scheme://foo"));
+
+  EXPECT_TRUE(IsValidStandardPresentationSource("https://google.com"));
+  EXPECT_TRUE(IsValidStandardPresentationSource("http://127.0.0.1"));
+  EXPECT_TRUE(IsValidStandardPresentationSource("http://localhost"));
+  EXPECT_FALSE(IsValidStandardPresentationSource("http://google.com"));
+
+  // Legacy Cast presentation URL is not a standard presentation source.
+  EXPECT_FALSE(IsValidStandardPresentationSource(
+      "https://google.com/cast#__castAppId__=DEADBEEF"));
 }
 
 TEST(MediaSourceTest, IsAutoJoinPresentationId) {
@@ -48,14 +66,14 @@
 }
 
 TEST(MediaSourceTest, ConstructorWithGURL) {
-  GURL test_url = GURL("http://google.com");
+  GURL test_url = GURL("https://google.com");
   MediaSource source1(test_url);
   EXPECT_EQ(test_url.spec(), source1.id());
   EXPECT_EQ(test_url, source1.url());
 }
 
 TEST(MediaSourceTest, ConstructorWithURLString) {
-  GURL test_url = GURL("http://google.com");
+  GURL test_url = GURL("https://google.com");
   MediaSource source1(test_url.spec());
   EXPECT_EQ(test_url.spec(), source1.id());
   EXPECT_EQ(test_url, source1.url());
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/media_router/common/media_source_unittest.cc b/components/media_router/common/media_source_unittest.cc
index 8595e87..5340b7b 100644
--- a/components/media_router/common/media_source_unittest.cc
+++ b/components/media_router/common/media_source_unittest.cc
@@ -33,6 +33,24 @@
   EXPECT_TRUE(IsValidPresentationUrl(GURL("cast://foo")));
   EXPECT_TRUE(IsValidPresentationUrl(GURL("cast:foo")));
   EXPECT_TRUE(IsValidPresentationUrl(GURL("remote-playback:foo")));
+
+  EXPECT_TRUE(IsValidPresentationUrl(GURL("http://127.0.0.1")));
+  EXPECT_TRUE(IsValidPresentationUrl(GURL("http://localhost")));
+  EXPECT_FALSE(IsValidPresentationUrl(GURL("http://google.com")));
+}
+
+TEST(MediaSourceTest, IsValidStandardPresentationSource) {
+  EXPECT_FALSE(IsValidStandardPresentationSource(""));
+  EXPECT_FALSE(IsValidStandardPresentationSource("unsupported-scheme://foo"));
+
+  EXPECT_TRUE(IsValidStandardPresentationSource("https://google.com"));
+  EXPECT_TRUE(IsValidStandardPresentationSource("http://127.0.0.1"));
+  EXPECT_TRUE(IsValidStandardPresentationSource("http://localhost"));
+  EXPECT_FALSE(IsValidStandardPresentationSource("http://google.com"));
+
+  // Legacy Cast presentation URL is not a standard presentation source.
+  EXPECT_FALSE(IsValidStandardPresentationSource(
+      "https://google.com/cast#__castAppId__=DEADBEEF"));
 }
 
 TEST(MediaSourceTest, IsAutoJoinPresentationId) {
@@ -48,14 +66,14 @@
 }
 
 TEST(MediaSourceTest, ConstructorWithGURL) {
-  GURL test_url = GURL("http://google.com");
+  GURL test_url = GURL("https://google.com");
   MediaSource source1(test_url);
   EXPECT_EQ(test_url.spec(), source1.id());
   EXPECT_EQ(test_url, source1.url());
 }
 
 TEST(MediaSourceTest, ConstructorWithURLString) {
-  GURL test_url = GURL("http://google.com");
+  GURL test_url = GURL("https://google.com");
   MediaSource source1(test_url.spec());
   EXPECT_EQ(test_url.spec(), source1.id());
   EXPECT_EQ(test_url, source1.url());
Loading diff…

Original Bug Report

reported by vm...@google.com

PNA Bypass and Local Network CSRF via Presentation API (Wired Display)

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can bypass Private Network Access (PNA) checks by initiating a Presentation API request to a local HTTP URL. If a user selects a Wired Display in the resulting Cast dialog, the browser performs a browser-initiated navigation to the target, bypassing PNA and enabling stateless CSRF against local network devices.

Affected files:

  • components/media_router/common/media_source.cc
  • chrome/browser/ui/media_router/presentation_receiver_window_controller.cc
  • content/browser/presentation/presentation_service_impl.cc
  • components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc
  • chrome/browser/media/router/providers/wired_display/wired_display_media_route_provider.cc
  • components/media_router/browser/presentation/presentation_navigation_policy.cc
  • chrome/browser/ui/media_router/media_route_starter.cc

Estimated timestamp from git blame: 2017-12-16

Vulnerability Overview

A potential vulnerability exists in the Presentation API’s Wired Display feature where a compromised renderer can bypass Private Network Access (PNA) restrictions. By crafting a malicious StartPresentation IPC message, an attacker can force the browser process to execute a browser-initiated GET request to an arbitrary local network IP address, enabling stateless Cross-Site Request Forgery (CSRF) against local devices (e.g., routers or IoT devices).

Potential Attack Steps

Note: The following steps are theoretical as our setup does not currently have the ability to run code to produce a working Proof of Concept.

  1. Renderer Compromise: An attacker exploits a separate vulnerability to gain code execution within a sandboxed renderer process.
  2. IPC Spoofing: The attacker directly sends a blink.mojom.PresentationService.StartPresentation Mojo IPC message to the browser. This bypasses renderer-side Blink checks (such as [SecureContext], Mixed Content restrictions, and User Activation requirements).
  3. Malicious URL Payload: In the IPC message, the attacker specifies a local network target in the presentation_urls field (e.g., http://192.168.1.1/admin/reboot?foo=bar).
  4. Browser-Side Validation Bypass: The browser receives the request in PresentationServiceImpl. The URL is validated against media_router::IsValidPresentationUrl, which explicitly allows the http:// scheme without checking for private/local IP addresses.
  5. Deceptive UI: The Cast dialog is presented to the user. It displays the origin of the requesting frame but hides the malicious destination URL.
  6. User Interaction: The user, believing the request is benign, selects a “Wired Display” (a connected secondary monitor) from the dialog.
  7. Browser-Initiated Navigation: WiredDisplayMediaRouteProvider creates a PresentationReceiverWindowController. To load the presentation, it calls LoadURLWithParams on a new Off-The-Record WebContents.
  8. PNA Bypass: Crucially, the content::NavigationController::LoadURLParams object defaults is_renderer_initiated to false. The network service treats this as a highly trusted browser-initiated navigation. Because there is no initiator ClientSecurityState attached, PNA checks are completely bypassed, and the HTTP GET request to the local network device is executed.

Impact

An attacker with a compromised renderer can perform stateless CSRF attacks (triggering state-changing GET requests) against devices on the victim’s local network. This bypasses the typical PNA protections designed to prevent external origins from reaching internal network addresses.

Suggested Fix

There are two primary ways to fix this issue:

  1. Propagate Initiator State: In PresentationReceiverWindowController::Start (and any other presentation controllers), explicitly set is_renderer_initiated = true on the LoadURLParams struct. Furthermore, ensure the initiator_origin and the appropriate ClientSecurityState are passed along from the original PresentationRequest so that the network service can enforce PNA checks correctly.
  2. Strict URL Validation: Update media_router::IsValidPresentationUrl or ControllerPresentationServiceDelegateImpl::StartPresentation to explicitly reject private/local IP addresses or localhost URLs unless the request originates from a securely contextualized, authenticated, or similarly privileged local source.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker