CVE-2026-8009
Overview
Background
- Presentation/Cast URL validation
media_router::IsValidPresentationUrlandmedia_router::IsValidStandardPresentationSourcedecide 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.
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
- Compromise a renderer the attacker runs code in a renderer able to issue Presentation API / Media Router requests.
- Supply an arbitrary local `http://` URL
provide a presentation URL such as
http://<private-network-host>that previously passedIsSchemeAllowed/IsValidStandardPresentationSourcesolely because the scheme washttp. - 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.
- Bypass PNA
pre-patch this let the renderer probe or interact with private-network HTTP resources; post-patch, non-localhost
httpis rejected bynet::IsLocalhost, blocking the bypass.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
TESTcomponents/media_router/common/media_source_unittest.cc |
modified |
Files Changed
components/media_router/common/media_source.cccomponents/media_router/common/media_source_unittest.cc
Audit Directions
- `SchemeIsHTTPOrHTTPS` over-acceptancesearch for validators that accept
httpandhttpstogether when onlyhttps(or localhosthttp) should be trusted, especially for inputs consumed in the browser process. - Browser-process requests and PNAaudit 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 checksflag allowlists that validate scheme without validating host for schemes (like cleartext
http) that are only safe against local/loopback targets.
Patch
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());
Regression Test / PoC
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());
Original Bug Report
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.ccchrome/browser/ui/media_router/presentation_receiver_window_controller.cccontent/browser/presentation/presentation_service_impl.cccomponents/media_router/browser/presentation/controller_presentation_service_delegate_impl.ccchrome/browser/media/router/providers/wired_display/wired_display_media_route_provider.cccomponents/media_router/browser/presentation/presentation_navigation_policy.ccchrome/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.
- Renderer Compromise: An attacker exploits a separate vulnerability to gain code execution within a sandboxed renderer process.
- IPC Spoofing: The attacker directly sends a
blink.mojom.PresentationService.StartPresentationMojo IPC message to the browser. This bypasses renderer-side Blink checks (such as[SecureContext], Mixed Content restrictions, and User Activation requirements). - Malicious URL Payload: In the IPC message, the attacker specifies a local network target in the
presentation_urlsfield (e.g.,http://192.168.1.1/admin/reboot?foo=bar). - Browser-Side Validation Bypass: The browser receives the request in
PresentationServiceImpl. The URL is validated againstmedia_router::IsValidPresentationUrl, which explicitly allows thehttp://scheme without checking for private/local IP addresses. - Deceptive UI: The Cast dialog is presented to the user. It displays the origin of the requesting frame but hides the malicious destination URL.
- User Interaction: The user, believing the request is benign, selects a “Wired Display” (a connected secondary monitor) from the dialog.
- Browser-Initiated Navigation:
WiredDisplayMediaRouteProvidercreates aPresentationReceiverWindowController. To load the presentation, it callsLoadURLWithParamson a new Off-The-Record WebContents. - PNA Bypass: Crucially, the
content::NavigationController::LoadURLParamsobject defaultsis_renderer_initiatedtofalse. The network service treats this as a highly trusted browser-initiated navigation. Because there is no initiatorClientSecurityStateattached, 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:
- Propagate Initiator State: In
PresentationReceiverWindowController::Start(and any other presentation controllers), explicitly setis_renderer_initiated = trueon theLoadURLParamsstruct. Furthermore, ensure theinitiator_originand the appropriateClientSecurityStateare passed along from the originalPresentationRequestso that the network service can enforce PNA checks correctly. - Strict URL Validation: Update
media_router::IsValidPresentationUrlorControllerPresentationServiceDelegateImpl::StartPresentationto 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.