Medium watch CVSS 8 chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactExternally controlled reference in DevTools
DescriptionExternally controlled reference in DevTools
ComponentDevTools
Sub-componentDevtools
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.
Tracker513134076
Fix commitf2ff15f45cb5 (chromium/src) +98/-4
CWECWE-610
CVSS vectorCVSS:3.1/AV:A/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-14 (per vendor advisory)
Fix landed2026-08-27 (public git)
Disclosed2026-09-22
Patch gap26 days — fix public in source before the advisory

Background

`CastDeviceProvider`
the Chrome component that populates the chrome://inspect device list from mDNS _googlecast._tcp.local Cast discovery responses.
mDNS discovery
multicast DNS service discovery in which any host on the reachable network (or a crafted responder) can advertise a _googlecast._tcp service along with a resolved IP address.
`IsPubliclyRoutable()`
a net::IPAddress predicate 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 CastAllowAllIPs feature 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.

Key insight
The single mistake was validating only that the mDNS-supplied address was well-formed (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

  1. 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.local record resolving to an address the attacker controls.
  2. Set a non-local resolved address The advertised ServiceDescription.ip_address is crafted to a publicly routable address (or loopback), which the pre-patch code accepted because it only failed the IsValid() check.
  3. Register the externally controlled reference CastDeviceProvider::OnDeviceChanged adds the crafted device to the chrome://inspect list, storing a reference target that points off the local network.
  4. 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

An attacker who can inject an mDNS Cast advertisement gains the ability to place an externally controlled, non-local reference into the DevTools 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

FunctionChangeNotes
DeviceListerDelegate
chrome/browser/devtools/device/cast_device_provider.h
modified
TEST_F
chrome/browser/devtools/device/cast_device_provider_unittest.cc
modified

Files Changed

  • chrome/browser/devtools/device/cast_device_provider_unittest.cc
  • chrome/browser/devtools/device/cast_device_provider.h
  • chrome/browser/devtools/device/cast_device_provider.cc

Audit Directions

  • mDNS/service-discovery inputs
    Audit any code that ingests ServiceDescription or other discovery-resolved addresses and treats them as trusted without a locality check beyond IsValid().
  • Policy parity across paths
    Look 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 peers
    Review 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.
From f2ff15f45cb5494168aeea5a73a084979fe46d31 Mon Sep 17 00:00:00 2001
From: Eric Leese <[email protected]>
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 <[email protected]>
Reviewed-by: Danil Somsikov <[email protected]>
Commit-Queue: Eric Leese <[email protected]>
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());
Loading diff…

Regression Test / PoC

shipped with the fix
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());
Loading diff…

Original Bug Report

reported by [email protected]

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.cc
  • chrome/browser/devtools/device/devtools_android_bridge.cc
  • chrome/browser/devtools/device/devtools_device_discovery.cc
  • chrome/browser/devtools/device/android_device_manager.cc
  • chrome/browser/devtools/device/tcp_device_provider.cc
  • chrome/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

  1. A victim user opens chrome://inspect#devices in a desktop build of Chrome.
  2. This initiates device discovery polling, causing CastDeviceProvider to start listening for mDNS queries for _googlecast._tcp.local services.
  3. 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.
  4. Every second, DevToolsDeviceDiscovery::DiscoveryRequest automatically initiates an HTTP GET /json/version request to the specified public IP.
  5. The raw HTTP response (headers and JSON body) is parsed directly within the unsandboxed browser process by HttpRequest in android_device_manager.cc and base::JSONReader in devtools_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.

View on issue tracker