CVE-2026-95376
Overview
Background
- `CastDeviceProvider`
- the Chrome component that populates the
chrome://inspectdevice list from mDNS_googlecast._tcp.localCast discovery responses. - mDNS discovery
- multicast DNS service discovery in which any host on the reachable network (or a crafted responder) can advertise a
_googlecast._tcpservice along with a resolved IP address. - `IsPubliclyRoutable()`
- a
net::IPAddresspredicate that returns true for globally routable (non-private, non-link-local) addresses, distinguishing internet-facing endpoints from local-network ones. - `GetCastAllowAllIPsPref`
- a media-router pref accessor gating the
CastAllowAllIPsfeature that, when enabled, permits Cast connections to any address including publicly routable ones.
Root Cause Analysis
In CastDeviceProvider::OnDeviceChanged, a discovered Cast service was accepted onto the chrome://inspect device list after only an ip_address.IsValid() check, with no restriction on where that address pointed. Because the resolved ip_address comes from attacker-influenceable mDNS metadata, a crafted _googlecast._tcp.local response could inject a device entry whose address is publicly routable (or loopback), so DevTools would treat an externally controlled, off-local-network reference as a queryable Cast device. The invariant violated is that Cast devices are expected to live on the local network, an assumption CastMediaSinkServiceImpl::OpenChannel already enforced for the Cast channel path but which this DevTools path did not.
The fix routes acceptance through the new OnDeviceChangedWithPref, which reads the CastAllowAllIPs pref and, unless it is set, rejects addresses that are IsPubliclyRoutable() or IsLoopback() in addition to invalid ones. This restores parity with the Cast channel policy and prevents non-local addresses from being registered as reference targets.
IsValid()) rather than that it was local, letting an externally controlled reference reach non-local addresses; the fix adds an IsPubliclyRoutable()/IsLoopback() rejection gated by the CastAllowAllIPs pref to constrain accepted devices to the local network.Attack Path
- Advertise a malicious Cast service
An attacker with access to the victim’s network segment (or able to inject an mDNS response) publishes a
_googlecast._tcp.localrecord resolving to an address the attacker controls. - Set a non-local resolved address
The advertised
ServiceDescription.ip_addressis crafted to a publicly routable address (or loopback), which the pre-patch code accepted because it only failed theIsValid()check. - Register the externally controlled reference
CastDeviceProvider::OnDeviceChangedadds the crafted device to thechrome://inspectlist, storing a reference target that points off the local network. - Leverage the injected device reference
Subsequent DevTools interaction with the injected entry (for example via
QueryDeviceInfo) directs Chrome’s Cast querying at the attacker-chosen address.(inferred)
Impact Assessment
chrome://inspect device list in the browser process, with the precondition that they can reach the victim’s mDNS discovery path and that CastAllowAllIPs is not enabled. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 8.0. The advisory does not state exploited-in-the-wild status or a severity rating here. Any escalation from injecting a non-local reference to a stronger primitive is not demonstrated by the patch. (inferred)Changed Functions
| Function | Change | Notes |
|---|---|---|
DeviceListerDelegatechrome/browser/devtools/device/cast_device_provider.h |
modified | |
TEST_Fchrome/browser/devtools/device/cast_device_provider_unittest.cc |
modified |
Files Changed
chrome/browser/devtools/device/cast_device_provider_unittest.ccchrome/browser/devtools/device/cast_device_provider.hchrome/browser/devtools/device/cast_device_provider.cc
Audit Directions
- mDNS/service-discovery inputsAudit any code that ingests
ServiceDescriptionor other discovery-resolved addresses and treats them as trusted without a locality check beyondIsValid(). - Policy parity across pathsLook for security checks enforced on one path (e.g.
CastMediaSinkServiceImpl::OpenChannel) but missing on a parallel path (DevTools, extensions) that consumes the same externally controlled data. - Reference registration from network peersReview lists or caches populated from network-advertised endpoints (
chrome://inspect, sink lists) to confirm publicly routable and loopback addresses are excluded unless an explicit allow-all pref is set.
Patch
From f2ff15f45cb5494168aeea5a73a084979fe46d31 Mon Sep 17 00:00:00 2001
From: Eric Leese <leese@chromium.org>
Date: Thu, 27 Aug 2026 06:40:49 -0700
Subject: [PATCH] [devtools] Restrict Cast device discovery to local addresses
CastDeviceProvider populates the chrome://inspect device list from mDNS
_googlecast._tcp.local responses but only checked the resolved address
with IsValid(). Cast devices are typically on a local network, so
additionally reject IsPubliclyRoutable() addresses unless CastAllowAllIPs
feature is enabled, matching what CastMediaSinkServiceImpl already does for
the Cast channel path.
TAG=agy
CONV=b9c82fd5-5928-41bf-af27-2defd29aba5b
Bug: 513134076
Change-Id: I03749b08a315213d3094c5c6474071becfce06ee
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8276082
Reviewed-by: Alex Rudenko <alexrudenko@chromium.org>
Reviewed-by: Danil Somsikov <dsv@chromium.org>
Commit-Queue: Eric Leese <leese@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1687166}
---
diff --git a/chrome/browser/devtools/device/cast_device_provider.cc b/chrome/browser/devtools/device/cast_device_provider.cc
index 56381dac..bbfbcc5 100644
--- a/chrome/browser/devtools/device/cast_device_provider.cc
+++ b/chrome/browser/devtools/device/cast_device_provider.cc
@@ -16,7 +16,9 @@
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/task/single_thread_task_runner.h"
+#include "chrome/browser/browser_process.h"
#include "chrome/browser/local_discovery/service_discovery_shared_client.h"
+#include "chrome/browser/media/router/media_router_feature.h"
#include "content/public/browser/browser_thread.h"
#include "net/base/host_port_pair.h"
#include "net/base/ip_address.h"
@@ -118,10 +120,15 @@
void OnDeviceChanged(const std::string& service_type,
bool added,
const ServiceDescription& service_description) override {
+ bool allow_all_ips = false;
+ if (g_browser_process && g_browser_process->local_state()) {
+ allow_all_ips = media_router::GetCastAllowAllIPsPref(
+ g_browser_process->local_state());
+ }
runner_->PostTask(
FROM_HERE,
- base::BindOnce(&CastDeviceProvider::OnDeviceChanged, provider_,
- service_type, added, service_description));
+ base::BindOnce(&CastDeviceProvider::OnDeviceChangedWithPref, provider_,
+ service_type, added, service_description, allow_all_ips));
}
void OnDeviceRemoved(const std::string& service_type,
@@ -196,13 +203,25 @@
const std::string& service_type,
bool added,
const ServiceDescription& service_description) {
+ OnDeviceChangedWithPref(service_type, added, service_description, false);
+}
+
+void CastDeviceProvider::OnDeviceChangedWithPref(
+ const std::string& service_type,
+ bool added,
+ const ServiceDescription& service_description,
+ bool allow_all_ips) {
VLOG(1) << "Device " << (added ? "added: " : "changed: ")
<< service_description.service_name;
if (service_description.service_type() != kCastServiceType)
return;
const net::IPAddress& ip_address = service_description.ip_address;
- if (!ip_address.IsValid()) {
- // An invalid IP address is not queryable.
+ if (!ip_address.IsValid() ||
+ (!allow_all_ips &&
+ (ip_address.IsPubliclyRoutable() || ip_address.IsLoopback()))) {
+ // Cast devices are discovered via mDNS and are expected to be on the local
+ // network, so only valid non-publicly-routable addresses are accepted.
+ // Requirements should match CastMediaSinkServiceImpl::OpenChannel.
return;
}
const std::string& name = service_description.service_name;
diff --git a/chrome/browser/devtools/device/cast_device_provider.h b/chrome/browser/devtools/device/cast_device_provider.h
index e36cdb9..2282fed 100644
--- a/chrome/browser/devtools/device/cast_device_provider.h
+++ b/chrome/browser/devtools/device/cast_device_provider.h
@@ -45,6 +45,12 @@
void OnDeviceCacheFlushed(const std::string& service_type) override;
void OnPermissionRejected() override;
+ void OnDeviceChangedWithPref(
+ const std::string& service_type,
+ bool added,
+ const local_discovery::ServiceDescription& service_description,
+ bool allow_all_ips);
+
private:
class DeviceListerDelegate;
friend class CastDeviceProviderTest;
diff --git a/chrome/browser/devtools/device/cast_device_provider_unittest.cc b/chrome/browser/devtools/device/cast_device_provider_unittest.cc
index 91de03d8..69a8ad3a 100644
--- a/chrome/browser/devtools/device/cast_device_provider_unittest.cc
+++ b/chrome/browser/devtools/device/cast_device_provider_unittest.cc
@@ -132,6 +132,75 @@
ASSERT_FALSE(was_run);
}
+TEST_F(CastDeviceProviderTest, LoopbackAddressIgnored) {
+ const std::string cast_service_type = "_googlecast._tcp.local";
+ const std::string loopback_host = "127.0.0.1";
+
+ ServiceDescription cast_service;
+ cast_service.service_name = "abcdefgh." + cast_service_type;
+ cast_service.address = net::HostPortPair(loopback_host, 8009);
+ cast_service.metadata.push_back("md=Fake Cast Device");
+ cast_service.metadata.push_back("fn=FakeCast1337");
+ ASSERT_TRUE(cast_service.ip_address.AssignFromIPLiteral(loopback_host));
+ ASSERT_TRUE(cast_service.ip_address.IsLoopback());
+
+ device_provider()->OnDeviceChanged(cast_service_type, true, cast_service);
+
+ // Cast devices are expected to be on the local network, so a service
+ // resolving to a publicly routable address must not be added.
+ EXPECT_TRUE(service_hostname_map().empty());
+ bool was_run = false;
+ device_provider()->QueryDeviceInfo(loopback_host,
+ base::BindOnce(&DummyCallback, &was_run));
+ EXPECT_FALSE(was_run);
+}
+
+TEST_F(CastDeviceProviderTest, PubliclyRoutableAddressIgnored) {
+ const std::string cast_service_type = "_googlecast._tcp.local";
+ const std::string public_host = "8.8.8.8";
+
+ ServiceDescription cast_service;
+ cast_service.service_name = "abcdefgh." + cast_service_type;
+ cast_service.address = net::HostPortPair(public_host, 8009);
+ cast_service.metadata.push_back("md=Fake Cast Device");
+ cast_service.metadata.push_back("fn=FakeCast1337");
+ ASSERT_TRUE(cast_service.ip_address.AssignFromIPLiteral(public_host));
+ ASSERT_TRUE(cast_service.ip_address.IsPubliclyRoutable());
+
+ device_provider()->OnDeviceChanged(cast_service_type, true, cast_service);
+
+ // Cast devices are expected to be on the local network, so a service
+ // resolving to a publicly routable address must not be added.
+ EXPECT_TRUE(service_hostname_map().empty());
+ bool was_run = false;
+ device_provider()->QueryDeviceInfo(public_host,
+ base::BindOnce(&DummyCallback, &was_run));
+ EXPECT_FALSE(was_run);
+}
+
+TEST_F(CastDeviceProviderTest, PubliclyRoutableAddressAcceptedWhenAllowed) {
+ const std::string cast_service_type = "_googlecast._tcp.local";
+ const std::string public_host = "8.8.8.8";
+
+ ServiceDescription cast_service;
+ cast_service.service_name = "abcdefgh." + cast_service_type;
+ cast_service.address = net::HostPortPair(public_host, 8009);
+ cast_service.metadata.push_back("md=Fake Cast Device");
+ cast_service.metadata.push_back("fn=FakeCast1337");
+ ASSERT_TRUE(cast_service.ip_address.AssignFromIPLiteral(public_host));
+ ASSERT_TRUE(cast_service.ip_address.IsPubliclyRoutable());
+
+ // Call OnDeviceChangedWithPref directly with allow_all_ips=true.
+ device_provider()->OnDeviceChangedWithPref(cast_service_type, true, cast_service, true);
+
+ // The service should be added since allow_all_ips is true.
+ EXPECT_FALSE(service_hostname_map().empty());
+ bool was_run = false;
+ device_provider()->QueryDeviceInfo(public_host,
+ base::BindOnce(&DummyCallback, &was_run));
+ EXPECT_TRUE(was_run);
+}
+
TEST_F(CastDeviceProviderTest, OnPermissionRejected) {
SimulateAddDeviceToHostnameMap();
EXPECT_FALSE(service_hostname_map().empty());
Regression Test / PoC
diff --git a/chrome/browser/devtools/device/cast_device_provider_unittest.cc b/chrome/browser/devtools/device/cast_device_provider_unittest.cc
index 91de03d8..69a8ad3a 100644
--- a/chrome/browser/devtools/device/cast_device_provider_unittest.cc
+++ b/chrome/browser/devtools/device/cast_device_provider_unittest.cc
@@ -132,6 +132,75 @@
ASSERT_FALSE(was_run);
}
+TEST_F(CastDeviceProviderTest, LoopbackAddressIgnored) {
+ const std::string cast_service_type = "_googlecast._tcp.local";
+ const std::string loopback_host = "127.0.0.1";
+
+ ServiceDescription cast_service;
+ cast_service.service_name = "abcdefgh." + cast_service_type;
+ cast_service.address = net::HostPortPair(loopback_host, 8009);
+ cast_service.metadata.push_back("md=Fake Cast Device");
+ cast_service.metadata.push_back("fn=FakeCast1337");
+ ASSERT_TRUE(cast_service.ip_address.AssignFromIPLiteral(loopback_host));
+ ASSERT_TRUE(cast_service.ip_address.IsLoopback());
+
+ device_provider()->OnDeviceChanged(cast_service_type, true, cast_service);
+
+ // Cast devices are expected to be on the local network, so a service
+ // resolving to a publicly routable address must not be added.
+ EXPECT_TRUE(service_hostname_map().empty());
+ bool was_run = false;
+ device_provider()->QueryDeviceInfo(loopback_host,
+ base::BindOnce(&DummyCallback, &was_run));
+ EXPECT_FALSE(was_run);
+}
+
+TEST_F(CastDeviceProviderTest, PubliclyRoutableAddressIgnored) {
+ const std::string cast_service_type = "_googlecast._tcp.local";
+ const std::string public_host = "8.8.8.8";
+
+ ServiceDescription cast_service;
+ cast_service.service_name = "abcdefgh." + cast_service_type;
+ cast_service.address = net::HostPortPair(public_host, 8009);
+ cast_service.metadata.push_back("md=Fake Cast Device");
+ cast_service.metadata.push_back("fn=FakeCast1337");
+ ASSERT_TRUE(cast_service.ip_address.AssignFromIPLiteral(public_host));
+ ASSERT_TRUE(cast_service.ip_address.IsPubliclyRoutable());
+
+ device_provider()->OnDeviceChanged(cast_service_type, true, cast_service);
+
+ // Cast devices are expected to be on the local network, so a service
+ // resolving to a publicly routable address must not be added.
+ EXPECT_TRUE(service_hostname_map().empty());
+ bool was_run = false;
+ device_provider()->QueryDeviceInfo(public_host,
+ base::BindOnce(&DummyCallback, &was_run));
+ EXPECT_FALSE(was_run);
+}
+
+TEST_F(CastDeviceProviderTest, PubliclyRoutableAddressAcceptedWhenAllowed) {
+ const std::string cast_service_type = "_googlecast._tcp.local";
+ const std::string public_host = "8.8.8.8";
+
+ ServiceDescription cast_service;
+ cast_service.service_name = "abcdefgh." + cast_service_type;
+ cast_service.address = net::HostPortPair(public_host, 8009);
+ cast_service.metadata.push_back("md=Fake Cast Device");
+ cast_service.metadata.push_back("fn=FakeCast1337");
+ ASSERT_TRUE(cast_service.ip_address.AssignFromIPLiteral(public_host));
+ ASSERT_TRUE(cast_service.ip_address.IsPubliclyRoutable());
+
+ // Call OnDeviceChangedWithPref directly with allow_all_ips=true.
+ device_provider()->OnDeviceChangedWithPref(cast_service_type, true, cast_service, true);
+
+ // The service should be added since allow_all_ips is true.
+ EXPECT_FALSE(service_hostname_map().empty());
+ bool was_run = false;
+ device_provider()->QueryDeviceInfo(public_host,
+ base::BindOnce(&DummyCallback, &was_run));
+ EXPECT_TRUE(was_run);
+}
+
TEST_F(CastDeviceProviderTest, OnPermissionRejected) {
SimulateAddDeviceToHostnameMap();
EXPECT_FALSE(service_hostname_map().empty());
Original Bug Report
SSRF and Untrusted Data Parsing in CastDeviceProvider via mDNS Spoofing
Project Fortify, 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 without the Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: CastDeviceProvider fails to validate that IP addresses discovered via mDNS are not publicly routable, allowing a local network attacker to trigger an SSRF. By spoofing a Cast device, an attacker can cause the browser process to make periodic HTTP requests to arbitrary public IPs and parse the responses in-process. This bypasses typical network isolation boundaries and exposes the browser process to untrusted network data.
Affected files:
chrome/browser/devtools/device/cast_device_provider.ccchrome/browser/devtools/device/devtools_android_bridge.ccchrome/browser/devtools/device/devtools_device_discovery.ccchrome/browser/devtools/device/android_device_manager.ccchrome/browser/devtools/device/tcp_device_provider.ccchrome/browser/ui/webui/inspect/inspect_ui.cc
Estimated timestamp from git blame: 2015-11-18
Summary
A potential vulnerability in CastDeviceProvider allows a local network attacker to perform Server-Side Request Forgery (SSRF) and force the browser process to parse untrusted network data. The provider accepts IP addresses from mDNS responses without verifying if they are restricted to the local network, enabling a spoofing attack that targets arbitrary public IP addresses.
Root Cause Analysis
In chrome/browser/devtools/device/cast_device_provider.cc, the CastDeviceProvider::OnDeviceChanged method processes resolved IP addresses from mDNS _googlecast._tcp.local service descriptions. While it validates the IP’s format, it lacks a check for whether the address is publicly routable.
// chrome/browser/devtools/device/cast_device_provider.cc:203-207
const net::IPAddress& ip_address = service_description.ip_address;
if (!ip_address.IsValid()) {
// An invalid IP address is not queryable.
return;
}
Other Cast-related components in Chromium (such as CastMediaSinkServiceImpl) explicitly use ip_address.IsPubliclyRoutable() to reject public addresses during discovery to prevent SSRF and cross-site tracking. The absence of this check here allows the DevToolsDeviceDiscovery logic to initiate TCP connections to any IP provided by an mDNS responder.
Potential Trigger Path
- A victim user opens
chrome://inspect#devicesin a desktop build of Chrome. - This initiates device discovery polling, causing
CastDeviceProviderto start listening for mDNS queries for_googlecast._tcp.localservices. - An attacker on the same local network segment responds to the mDNS query with a spoofed A record resolving to a targeted public IP address (e.g., a cloud metadata service or an attacker-controlled server) on port 9222.
- Every second,
DevToolsDeviceDiscovery::DiscoveryRequestautomatically initiates an HTTPGET /json/versionrequest to the specified public IP. - The raw HTTP response (headers and JSON body) is parsed directly within the unsandboxed browser process by
HttpRequestinandroid_device_manager.ccandbase::JSONReaderindevtools_device_discovery.cc.
Impact
An attacker can cause the browser process to make outbound TCP connections and HTTP requests to arbitrary IPs on port 9222. Furthermore, because the responses are parsed in the privileged browser process, any potential vulnerabilities in the HTTP or JSON parsing logic could lead to memory corruption in the most privileged part of the browser.
Suggested Fix
Modify CastDeviceProvider::OnDeviceChanged in chrome/browser/devtools/device/cast_device_provider.cc to reject addresses that are publicly routable:
if (!ip_address.IsValid() || ip_address.IsPubliclyRoutable()) {
return;
}
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.