CVE-2026-17870
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.cc |
modified | |
TEST_Pchrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc |
modified | |
forchrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc |
modified |
Files Changed
chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.ccchrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc
Patch
From f6f09166cf447580d798baca7c6d4ccb523bf16f Mon Sep 17 00:00:00 2001
From: Muyao Xu <muyaoxu@google.com>
Date: Mon, 22 Jun 2026 13:18:33 -0700
Subject: [PATCH] [MediaRouter] Block loopback IP addresses in Cast discovery
This CL hardens the IP validation in CastMediaSinkServiceImpl to
publicly routable addresses and loopback IP addresses.
Bug: 521784856
Change-Id: I73e32932e5976a30d5b449c7fd4c3cd3a1818032
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7958165
Reviewed-by: Jordan Bayles <jophba@chromium.org>
Commit-Queue: Muyao Xu <muyaoxu@google.com>
Cr-Commit-Position: refs/heads/main@{#1650528}
---
diff --git a/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.cc b/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.cc
index 170258b..4a87216 100644
--- a/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.cc
+++ b/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.cc
@@ -478,14 +478,16 @@
DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
const net::IPEndPoint& ip_endpoint = cast_sink.cast_data().ip_endpoint;
- if (!allow_all_ips_ && ip_endpoint.address().IsPubliclyRoutable()) {
+ if (!allow_all_ips_ && (ip_endpoint.address().IsPubliclyRoutable() ||
+ ip_endpoint.address().IsLoopback())) {
LoggerList::GetInstance()->Log(
LoggerImpl::Severity::kWarning, mojom::LogCategory::kDiscovery,
kLoggerComponent,
base::StrCat({"Did not open a channel to the IP endpoint: ",
- ip_endpoint.ToString(),
- " because it is publicly "
- "routable."}),
+ ip_endpoint.ToString(), " because it is ",
+ ip_endpoint.address().IsPubliclyRoutable()
+ ? "publicly routable."
+ : "a loopback address."}),
cast_sink.sink().id(), "", "");
if (callback) {
std::move(callback).Run(false);
diff --git a/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc b/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc
index 0283add..13c8317 100644
--- a/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc
+++ b/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc
@@ -1577,30 +1577,34 @@
mock_time_task_runner_->FastForwardUntilNoTasksRemain();
}
-TEST_P(CastMediaSinkServiceImplTest,
- TestOpenChannelFailsForPubliclyRoutableIP) {
- MediaSinkInternal cast_sink = CreateCastSink(1);
+TEST_P(CastMediaSinkServiceImplTest, TestOpenChannelFailsForInvalidIP) {
+ std::vector<std::string> invalid_ips = {kPubliclyRoutableIPv4Address,
+ "127.0.0.1"};
- net::IPAddress address;
- EXPECT_TRUE(address.AssignFromIPLiteral(kPubliclyRoutableIPv4Address));
- ASSERT_TRUE(address.IsValid());
+ for (const auto& ip_str : invalid_ips) {
+ MediaSinkInternal cast_sink = CreateCastSink(1);
- auto ip_endpoint = net::IPEndPoint(address, 8009);
- ASSERT_TRUE(ip_endpoint.address().IsPubliclyRoutable());
+ net::IPAddress address;
+ EXPECT_TRUE(address.AssignFromIPLiteral(ip_str));
+ ASSERT_TRUE(address.IsValid());
- CastSinkExtraData extra_data = cast_sink.cast_data();
- extra_data.ip_endpoint = ip_endpoint;
- cast_sink.set_cast_data(extra_data);
+ auto ip_endpoint = net::IPEndPoint(address, 8009);
- MockBoolCallback mock_callback;
- EXPECT_CALL(mock_callback, Run(false)).Times(1);
+ CastSinkExtraData extra_data = cast_sink.cast_data();
+ extra_data.ip_endpoint = ip_endpoint;
+ cast_sink.set_cast_data(extra_data);
- // No pending sink
- EXPECT_CALL(*mock_cast_socket_service_, OpenSocket_(ip_endpoint, _)).Times(0);
- media_sink_service_impl_.OpenChannel(
- cast_sink, nullptr, CastMediaSinkServiceImpl::SinkSource::kMdns,
- mock_callback.Get(),
- media_sink_service_impl_.CreateCastSocketOpenParams(cast_sink));
+ MockBoolCallback mock_callback;
+ EXPECT_CALL(mock_callback, Run(false)).Times(1);
+
+ // No pending sink
+ EXPECT_CALL(*mock_cast_socket_service_, OpenSocket_(ip_endpoint, _))
+ .Times(0);
+ media_sink_service_impl_.OpenChannel(
+ cast_sink, nullptr, CastMediaSinkServiceImpl::SinkSource::kMdns,
+ mock_callback.Get(),
+ media_sink_service_impl_.CreateCastSocketOpenParams(cast_sink));
+ }
}
INSTANTIATE_TEST_SUITE_P(DialMediaSinkServiceEnabled,
Regression Test / PoC
diff --git a/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc b/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc
index 0283add..13c8317 100644
--- a/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc
+++ b/chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl_unittest.cc
@@ -1577,30 +1577,34 @@
mock_time_task_runner_->FastForwardUntilNoTasksRemain();
}
-TEST_P(CastMediaSinkServiceImplTest,
- TestOpenChannelFailsForPubliclyRoutableIP) {
- MediaSinkInternal cast_sink = CreateCastSink(1);
+TEST_P(CastMediaSinkServiceImplTest, TestOpenChannelFailsForInvalidIP) {
+ std::vector<std::string> invalid_ips = {kPubliclyRoutableIPv4Address,
+ "127.0.0.1"};
- net::IPAddress address;
- EXPECT_TRUE(address.AssignFromIPLiteral(kPubliclyRoutableIPv4Address));
- ASSERT_TRUE(address.IsValid());
+ for (const auto& ip_str : invalid_ips) {
+ MediaSinkInternal cast_sink = CreateCastSink(1);
- auto ip_endpoint = net::IPEndPoint(address, 8009);
- ASSERT_TRUE(ip_endpoint.address().IsPubliclyRoutable());
+ net::IPAddress address;
+ EXPECT_TRUE(address.AssignFromIPLiteral(ip_str));
+ ASSERT_TRUE(address.IsValid());
- CastSinkExtraData extra_data = cast_sink.cast_data();
- extra_data.ip_endpoint = ip_endpoint;
- cast_sink.set_cast_data(extra_data);
+ auto ip_endpoint = net::IPEndPoint(address, 8009);
- MockBoolCallback mock_callback;
- EXPECT_CALL(mock_callback, Run(false)).Times(1);
+ CastSinkExtraData extra_data = cast_sink.cast_data();
+ extra_data.ip_endpoint = ip_endpoint;
+ cast_sink.set_cast_data(extra_data);
- // No pending sink
- EXPECT_CALL(*mock_cast_socket_service_, OpenSocket_(ip_endpoint, _)).Times(0);
- media_sink_service_impl_.OpenChannel(
- cast_sink, nullptr, CastMediaSinkServiceImpl::SinkSource::kMdns,
- mock_callback.Get(),
- media_sink_service_impl_.CreateCastSocketOpenParams(cast_sink));
+ MockBoolCallback mock_callback;
+ EXPECT_CALL(mock_callback, Run(false)).Times(1);
+
+ // No pending sink
+ EXPECT_CALL(*mock_cast_socket_service_, OpenSocket_(ip_endpoint, _))
+ .Times(0);
+ media_sink_service_impl_.OpenChannel(
+ cast_sink, nullptr, CastMediaSinkServiceImpl::SinkSource::kMdns,
+ mock_callback.Get(),
+ media_sink_service_impl_.CreateCastSocketOpenParams(cast_sink));
+ }
}
INSTANTIATE_TEST_SUITE_P(DialMediaSinkServiceEnabled,
Original Bug Report
Blind SSRF and Internal Port Probing via Cast mDNS Discovery
Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: Chrome’s Cast discovery service automatically initiates TCP and TLS connections to IPs and ports specified in local mDNS responses. The IP validation check explicitly permits connections to all private (RFC 1918) and loopback addresses. A local network attacker can broadcast malicious mDNS records to use the browser as a blind SSRF proxy, probing internal ports and bypassing Private Network Access (PNA) protections.
Affected files:
chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.ccchrome/browser/media/router/discovery/mdns/media_sink_util.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Summary
A potential vulnerability in Chrome’s Cast Media Sink discovery service allows an attacker on the local network to perform blind Server-Side Request Forgery (SSRF) and internal port probing. By broadcasting malicious mDNS responses for the _googlecast._tcp.local service type, an attacker can induce Chrome into initiating TCP connections to arbitrary internal IP addresses and ports, including loopback (127.0.0.1) and private IP ranges.
Technical Details
The Cast discovery service, running in the browser process, listens for mDNS responses to discover Cast devices. When a new service is discovered, Chrome attempts to establish a Cast channel (initially a TCP connection followed by a TLS handshake) to the provided endpoint.
- mDNS Parsing: When a UDP packet on port 5353 is received,
DnsSdRegistry::DispatchApiEventnotifiesCastMediaSinkService::OnDnsSdEvent(chrome/browser/media/router/discovery/mdns/cast_media_sink_service.cc:163). - IP Extraction:
CreateCastMediaSinkparses the IP address and port from the mDNS response without restricting it to specific ranges (chrome/browser/media/router/discovery/mdns/media_sink_util.cc:41). - Flawed Validation: The connection attempt is routed to
CastMediaSinkServiceImpl::OpenChannel(chrome/browser/media/router/discovery/mdns/cast_media_sink_service_impl.cc:472). A security check is performed:if (!allow_all_ips_ && ip_endpoint.address().IsPubliclyRoutable())(line 481). - Bypass: Because the attacker provides a private IP (e.g.,
10.x.x.x) or loopback address (127.0.0.1),net::IPAddress::IsPubliclyRoutable()returnsfalse(net/base/ip_address.cc:224). The condition fails, and the connection is permitted. - Socket Creation: Chrome creates a
CastSocketImpland sends a Mojo IPC request to the Network Service to create aTCPConnectedSocketto the attacker-specified internal IP and port.
While the payload sent upon connection is limited to a TLS ClientHello (and potentially a Cast AuthChallenge), the initiation of the TCP connection itself bypasses Private Network Access (PNA) protections.
Suggested Attacker Steps
Note: These are potential steps based on code analysis; our tooling agent does not run code to verify them.
- An attacker resides on the same local network as the victim.
- The attacker broadcasts crafted mDNS UDP packets on port 5353, advertising
_googlecast._tcp.localservices with target IPs corresponding to the victim’s loopback address or internal VPN hosts, specifying various target ports. - Chrome receives and parses the mDNS broadcast, automatically initiating TCP connection attempts to the specified internal IPs/ports via the system network context.
- The attacker infers the status of the target ports (open/closed) by measuring the timing of subsequent mDNS broadcasts or using mDNS known-answer suppression side channels.
Suggested Fix
The IP validation logic in CastMediaSinkServiceImpl::OpenChannel should be hardened. While Cast devices reside on private networks, allowing connections to loopback (127.0.0.0/8 and ::1) is unnecessary and should be blocked. Additionally, to mitigate scanning of arbitrary internal networks, Chrome could restrict automatic connections to the local subnet the mDNS packet originated from, or restrict connections to typical Cast device ports (e.g., 8009) unless explicitly initiated by user interaction.
Evaluated with Chrome root at commit: 2155cb00003ec35716a76ed3246eae995f87b7ff
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.