CVE-2026-11259
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/media/router/providers/cast/cast_media_controller.cc |
modified | |
TEST_Fchrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc |
modified |
Files Changed
chrome/browser/media/router/providers/cast/cast_media_controller.ccchrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc
Patch
From 16b92652be83127eda566eee308ee399bf9b12bd Mon Sep 17 00:00:00 2001 From: Muyao Xu <[email protected]> Date: Tue, 21 Apr 2026 18:23:32 -0700 Subject: [PATCH] [Cast] Fix blind SSRF / LNA bypass via Cast MEDIA_STATUS image URL This change adds URL validation in CastMediaController::UpdateMediaStatus to ensure that the media image URL uses an allowed scheme (HTTP/HTTPS) and is not a localhost or private IP address. It also updates the BitmapFetcher in CastMediaNotificationItem to use a ClientSecurityState that blocks Local Network Access, preventing local requests from bypassing LNA enforcement. Bug: 499215943 Change-Id: I1e686d4b857263e5fff9369ed205f4e62afdff7c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7763663 Reviewed-by: Frank Liberato <[email protected]> Commit-Queue: Muyao Xu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1618595} --- diff --git a/chrome/browser/media/router/providers/cast/cast_media_controller.cc b/chrome/browser/media/router/providers/cast/cast_media_controller.cc index 6d43582..5a5fb97f 100644 --- a/chrome/browser/media/router/providers/cast/cast_media_controller.cc +++ b/chrome/browser/media/router/providers/cast/cast_media_controller.cc @@ -14,6 +14,9 @@ #include "chrome/browser/media/router/providers/cast/cast_internal_message_util.h" #include "components/media_router/common/providers/cast/channel/cast_message_util.h" #include "components/media_router/common/providers/cast/channel/enum_table.h" +#include "net/base/ip_address.h" +#include "net/base/url_util.h" +#include "url/url_constants.h" using cast_channel::V2MessageType; @@ -248,7 +251,15 @@ if (!url_string) { continue; } - media_status_.images.emplace_back(std::in_place, GURL(*url_string), + + GURL url(*url_string); + + // Ensure the URL is valid and uses a safe scheme (HTTP/HTTPS). + if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) { + continue; + } + + media_status_.images.emplace_back(std::in_place, url, GetValidSize(image_dict)); } } diff --git a/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc b/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc index dc72903..61b552e7 100644 --- a/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc +++ b/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc @@ -411,6 +411,29 @@ VerifyAndClearExpectations(); } +TEST_F(CastMediaControllerTest, IgnoreInsecureImages) { + mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus(); + expected_status->images.emplace_back( + std::in_place, GURL("https://example.com/1.png"), gfx::Size(123, 456)); + expected_status->images.emplace_back( + std::in_place, GURL("http://localhost/2.png"), gfx::Size(123, 456)); + expected_status->images.emplace_back( + std::in_place, GURL("data:image/png;base64,iVBORw0KGgo"), + gfx::Size(123, 456)); + + base::DictValue status_value = CreateMediaStatus(*expected_status); + + EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_)) + .WillOnce([&](const mojom::MediaStatusPtr& status) { + ASSERT_EQ(2u, status->images.size()); + EXPECT_EQ("https://example.com/1.png", + status->images.at(0)->url.spec()); + EXPECT_EQ("http://localhost/2.png", status->images.at(1)->url.spec()); + }); + SetMediaStatus(std::move(status_value)); + VerifyAndClearExpectations(); +} + TEST_F(CastMediaControllerTest, UpdateVolumeStatus) { auto session = CreateSampleSession(); const float session_volume =
Regression Test / PoC
diff --git a/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc b/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc
index dc72903..61b552e7 100644
--- a/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/cast_media_controller_unittest.cc
@@ -411,6 +411,29 @@
VerifyAndClearExpectations();
}
+TEST_F(CastMediaControllerTest, IgnoreInsecureImages) {
+ mojom::MediaStatusPtr expected_status = CreateSampleMediaStatus();
+ expected_status->images.emplace_back(
+ std::in_place, GURL("https://example.com/1.png"), gfx::Size(123, 456));
+ expected_status->images.emplace_back(
+ std::in_place, GURL("http://localhost/2.png"), gfx::Size(123, 456));
+ expected_status->images.emplace_back(
+ std::in_place, GURL("data:image/png;base64,iVBORw0KGgo"),
+ gfx::Size(123, 456));
+
+ base::DictValue status_value = CreateMediaStatus(*expected_status);
+
+ EXPECT_CALL(*status_observer_, OnMediaStatusUpdated(_))
+ .WillOnce([&](const mojom::MediaStatusPtr& status) {
+ ASSERT_EQ(2u, status->images.size());
+ EXPECT_EQ("https://example.com/1.png",
+ status->images.at(0)->url.spec());
+ EXPECT_EQ("http://localhost/2.png", status->images.at(1)->url.spec());
+ });
+ SetMediaStatus(std::move(status_value));
+ VerifyAndClearExpectations();
+}
+
TEST_F(CastMediaControllerTest, UpdateVolumeStatus) {
auto session = CreateSampleSession();
const float session_volume =
Original Bug Report
Browser-process blind SSRF / LNA bypass via Cast MEDIA_STATUS image URL
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 security team.
Overview: The Cast Media Controller does not validate image URLs received in MEDIA_STATUS messages from Cast receiver applications. This potentially allows a malicious receiver app to trigger the Chrome browser process to perform blind GET requests to arbitrary internal addresses (e.g., localhost, private IPs), bypassing Local Network Access (LNA) protections.
Affected files:
chrome/browser/media/router/providers/cast/cast_media_controller.ccchrome/browser/ui/global_media_controls/cast_media_notification_item.cc
Estimated timestamp from git blame: 2024-12-23
Summary
A malicious Cast receiver application can potentially induce the Chrome browser process to issue HTTP/HTTPS GET requests to arbitrary addresses, including localhost (127.0.0.1), RFC1918 private ranges, and cloud metadata endpoints. This is achieved by sending a MEDIA_STATUS message with a crafted media.metadata.images[].url field.
The vulnerability stems from the URL being extracted from the network-provided JSON payload and converted to a GURL without validation (such as checking url.is_valid() or url.SchemeIsHTTPOrHTTPS()). This URL is subsequently downloaded by the CastMediaNotificationItem using a BitmapFetcher backed by a privileged browser-process URLLoaderFactory. Because this browser-initiated request lacks a client_security_state, it bypasses Local Network Access (LNA) enforcement.
Technical Details
1. Lack of URL Validation
In chrome/browser/media/router/providers/cast/cast_media_controller.cc, the UpdateMediaStatus function parses the MEDIA_STATUS message from the Cast receiver. It extracts the image URL from the JSON dictionary and populates the media_status_ object. The URL is instantiated as a GURL directly from the string without verifying its scheme or safety:
// chrome/browser/media/router/providers/cast/cast_media_controller.cc
const std::string* url_string = image_dict.FindString("url");
if (!url_string) continue;
media_status_.images.emplace_back(std::in_place, GURL(*url_string), ...);
2. Mojo Propagation and Automatic Trigger
The unvalidated media_status_ is then sent via a Mojo IPC call to registered observers, including CastMediaNotificationItem. When the status update arrives, CastMediaNotificationItem::OnMediaStatusUpdated automatically triggers a background download of the first image URL to display in the Global Media Controls UI:
// chrome/browser/ui/global_media_controls/cast_media_notification_item.cc
if (status->images.empty()) { ... } else {
image_downloader_.Download(status->images.at(0)->url);
}
3. Privileged Fetching and LNA Bypass
The ImageDownloader uses a BitmapFetcher initialized with GetURLLoaderFactoryForBrowserProcess():
url_loader_factory_(profile->GetDefaultStoragePartition()->GetURLLoaderFactoryForBrowserProcess())
The browser-process URLLoaderFactory is highly privileged. Because the ResourceRequest created by BitmapFetcher does not specify a client_security_state, the network service’s LocalNetworkAccessChecker defaults to allowing the request:
// services/network/local_network_access_checker.cc
if (!client_security_state_) {
return Result::kAllowedMissingClientSecurityState;
}
This bypasses standard protections that would prevent web-influenced requests from reaching the local network or loopback interface.
Potential Attack Scenario
- An attacker registers a malicious Cast receiver app and hosts it on their server.
- A victim visits a page controlled by the attacker that uses the Cast Sender SDK to launch the attacker’s receiver app ID.
- The victim initiates a Cast session (e.g., by clicking the Cast button and selecting their device).
- The malicious receiver app sends a
MEDIA_STATUSmessage containing an internal URL (e.g.,http://127.0.0.1:9222/json/version). - The Chrome browser process parses the message and automatically fetches the URL. Since the fetcher follows redirects by default, an attacker can also use a 302 redirect from a public HTTPS URL to an internal HTTP URL.
- The browser process executes a blind GET request against the targeted internal service.
Suggested Fix
Add URL validation in CastMediaController::UpdateMediaStatus before adding the image to the media_status_ object. Ensure the URL is valid and uses an allowed scheme:
GURL url(*url_string);
if (!url.is_valid() || !url.SchemeIsHTTPOrHTTPS()) {
continue;
}
media_status_.images.emplace_back(std::in_place, url, GetValidSize(image_dict));
Additionally, consider whether the BitmapFetcher used for media notifications should be restricted from accessing local network addresses, potentially by providing a restricted client_security_state to the URLLoaderFactoryParams or the ResourceRequest.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.