Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Cast
DescriptionInappropriate implementation in Cast
ComponentCast
Bug ClassLogic Error
Tracker496555077
Fix commit52c5e880feae (chromium/src) +41/-9
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

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
From 52c5e880feae5309309788125965c11c4fc72036 Mon Sep 17 00:00:00 2001
From: mark a. foltz <[email protected]>
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 <[email protected]>
Commit-Queue: Mark Foltz <[email protected]>
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 [email protected]

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