CVE-2026-17852
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchrome/browser/ui/global_media_controls/test_helper.cc |
modified | |
ifchrome/browser/ui/media_router/media_route_starter.cc |
modified |
Files Changed
chrome/browser/ui/global_media_controls/test_helper.ccchrome/browser/ui/global_media_controls/test_helper.hchrome/browser/ui/media_router/media_route_starter.ccchrome/browser/ui/media_router/media_route_starter_unittest.cc
Patch
From 52a0d0f817370709072127cb87b5c6edb875ce16 Mon Sep 17 00:00:00 2001 From: Muyao Xu <[email protected]> Date: Wed, 03 Jun 2026 16:47:41 -0700 Subject: [PATCH] [MediaRouter] Skip default observation for explicit requests When MediaRouteStarter is initialized with an explicit presentation context, default presentation request updates from the page might overwrite the active StartPresentationContext. This CL prevents MediaRouteStarter from observing default presentation request updates when handling an explicit StartPresentationContext. Bug: b:519348818 Change-Id: I8872fd4d398b163882f0540c5e9a4a7d4f225abf Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7899527 Reviewed-by: Tommy Steimel <[email protected]> Commit-Queue: Muyao Xu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1641314} --- diff --git a/chrome/browser/ui/global_media_controls/test_helper.cc b/chrome/browser/ui/global_media_controls/test_helper.cc index 468cdd6..8305311 100644 --- a/chrome/browser/ui/global_media_controls/test_helper.cc +++ b/chrome/browser/ui/global_media_controls/test_helper.cc @@ -29,6 +29,13 @@ default_presentation_request_ = request; } +void MockWebContentsPresentationManager::NotifyDefaultPresentationChanged( + const content::PresentationRequest* request) { + for (auto& observer : observers_) { + observer.OnDefaultPresentationChanged(request); + } +} + void MockWebContentsPresentationManager::NotifyMediaRoutesChanged( const std::vector<media_router::MediaRoute>& routes) { for (auto& observer : observers_) { diff --git a/chrome/browser/ui/global_media_controls/test_helper.h b/chrome/browser/ui/global_media_controls/test_helper.h index 9a7fe61..289282d3 100644 --- a/chrome/browser/ui/global_media_controls/test_helper.h +++ b/chrome/browser/ui/global_media_controls/test_helper.h @@ -22,6 +22,8 @@ const std::vector<media_router::MediaRoute>& routes); void SetDefaultPresentationRequest( const content::PresentationRequest& request); + void NotifyDefaultPresentationChanged( + const content::PresentationRequest* request); // WebContentsPresentationManager implementation. bool HasDefaultPresentationRequest() const override; diff --git a/chrome/browser/ui/media_router/media_route_starter.cc b/chrome/browser/ui/media_router/media_route_starter.cc index 4e5f4a1..cb98078 100644 --- a/chrome/browser/ui/media_router/media_route_starter.cc +++ b/chrome/browser/ui/media_router/media_route_starter.cc @@ -77,7 +77,7 @@ : nullptr), query_result_manager_( std::make_unique<QueryResultManager>(GetMediaRouter())) { - if (presentation_manager_) { + if (presentation_manager_ && !start_presentation_context_) { presentation_manager_->AddObserver(this); } InitPresentationSources(params.initial_modes); diff --git a/chrome/browser/ui/media_router/media_route_starter_unittest.cc b/chrome/browser/ui/media_router/media_route_starter_unittest.cc index d1348e3..9bceab5 100644 --- a/chrome/browser/ui/media_router/media_route_starter_unittest.cc +++ b/chrome/browser/ui/media_router/media_route_starter_unittest.cc @@ -923,4 +923,32 @@ MediaCastMode::DESKTOP_MIRROR)); } +TEST_F(MediaRouteStarterTest, + StartPresentationContextDoesNotObserveDefaultPresentationChanges) { + const std::string kVictimUrl = "https://victim.example/recv"; + const std::string kVictimOrigin = "https://victim.example"; + content::PresentationRequest victim_request = + CreatePresentationRequest(kVictimUrl, kVictimOrigin); + auto start_presentation_context = + CreateStartPresentationContext(victim_request); + + CreateStarter(MediaRouterUIParameters(kDefaultModes, web_contents(), + std::move(start_presentation_context))); + + EXPECT_EQ(u"victim.example", + media_route_starter()->GetPresentationRequestSourceName()); + + const std::string kAttackerUrl = "https://attacker.example/recv"; + const std::string kAttackerOrigin = "https://attacker.example"; + content::PresentationRequest attacker_request = + CreatePresentationRequest(kAttackerUrl, kAttackerOrigin); + presentation_manager()->NotifyDefaultPresentationChanged(&attacker_request); + + // The source name should remain unchanged because MediaRouteStarter does not + // observe default presentation request changes. + EXPECT_EQ(u"victim.example", + media_route_starter()->GetPresentationRequestSourceName()); + EXPECT_CALL(*this, RequestError(_)); +} + } // namespace media_router
Regression Test / PoC
diff --git a/chrome/browser/ui/media_router/media_route_starter_unittest.cc b/chrome/browser/ui/media_router/media_route_starter_unittest.cc
index d1348e3..9bceab5 100644
--- a/chrome/browser/ui/media_router/media_route_starter_unittest.cc
+++ b/chrome/browser/ui/media_router/media_route_starter_unittest.cc
@@ -923,4 +923,32 @@
MediaCastMode::DESKTOP_MIRROR));
}
+TEST_F(MediaRouteStarterTest,
+ StartPresentationContextDoesNotObserveDefaultPresentationChanges) {
+ const std::string kVictimUrl = "https://victim.example/recv";
+ const std::string kVictimOrigin = "https://victim.example";
+ content::PresentationRequest victim_request =
+ CreatePresentationRequest(kVictimUrl, kVictimOrigin);
+ auto start_presentation_context =
+ CreateStartPresentationContext(victim_request);
+
+ CreateStarter(MediaRouterUIParameters(kDefaultModes, web_contents(),
+ std::move(start_presentation_context)));
+
+ EXPECT_EQ(u"victim.example",
+ media_route_starter()->GetPresentationRequestSourceName());
+
+ const std::string kAttackerUrl = "https://attacker.example/recv";
+ const std::string kAttackerOrigin = "https://attacker.example";
+ content::PresentationRequest attacker_request =
+ CreatePresentationRequest(kAttackerUrl, kAttackerOrigin);
+ presentation_manager()->NotifyDefaultPresentationChanged(&attacker_request);
+
+ // The source name should remain unchanged because MediaRouteStarter does not
+ // observe default presentation request changes.
+ EXPECT_EQ(u"victim.example",
+ media_route_starter()->GetPresentationRequestSourceName());
+ EXPECT_CALL(*this, RequestError(_));
+}
+
} // namespace media_router
Original Bug Report
Potential MediaRouteStarter retargeting of subframe PresentationRequest to top-frame defaultRequest
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: MediaRouteStarter unconditionally registers as an observer of WebContentsPresentationManager’s default presentation requests. If a top-frame modifies its defaultRequest while a subframe’s explicit StartPresentationContext is active, the starter’s configuration is overwritten with the top-frame’s URL/origin. This can potentially allow a top-frame to spoof the origin in the Cast dialog and inject a cross-origin PresentationConnection into the victim subframe.
Affected files:
chrome/browser/ui/media_router/media_route_starter.cccomponents/media_router/browser/presentation/start_presentation_context.cc
Estimated timestamp from git blame: 2022-04-07
Problem Description
There is a potential logical security vulnerability in the Chromium Media Router component where a subframe’s explicit presentation request (initiated via PresentationRequest.start()) can be hijacked and retargeted by the parent/top frame.
When a subframe requests a presentation start, MediaNotificationService creates a MediaRouterUI controller with the subframe’s StartPresentationContext. This, in turn, instantiates a MediaRouteStarter to manage the lifecycle of the route creation.
However, in chrome/browser/ui/media_router/media_route_starter.cc:
-
Unconditional Observer Registration: The constructor of
MediaRouteStarterregisters itself unconditionally as an observer of the top-frame’s default presentation manager (WebContentsPresentationManager), regardless of whether it was initialized specifically for a subframe’s explicitstart_presentation_context_:// chrome/browser/ui/media_router/media_route_starter.cc:80-82 if (presentation_manager_) { presentation_manager_->AddObserver(this); } -
State Poisoning via Default Request Updates: If the top-frame changes its default request (by setting
navigator.presentation.defaultRequestwithout requiring a user gesture) while the Cast dialog is open,MediaRouteStarter::OnDefaultPresentationChangedis called. It lacks any check for whetherstart_presentation_context_is active and immediately overwritespresentation_request_and the query manager’s presentation sources with the attacker-controlled top-frame request:// chrome/browser/ui/media_router/media_route_starter.cc:264-285 void MediaRouteStarter::OnDefaultPresentationChanged( const content::PresentationRequest* presentation_request) { if (presentation_request) { ... presentation_request_ = *presentation_request; // Overwrites subframe's request! GetQueryResultManager()->SetSourcesForCastMode( MediaCastMode::PRESENTATION, sources, presentation_request_->frame_origin); } } -
Resulting Exploitation Paths:
- Origin Spoofing: The browser-drawn Global Media Controls (GMC) card origin label displays the victim subframe’s origin (
victim.example) because it is set once on dialog creation and not refreshed whenOnDefaultPresentationChangedfires. However, clicking a Cast device launches the attacker’s receiver URL. - PresentationConnection Injection: Once the route is established, the response is delivered to the subframe via
StartPresentationContext::HandleRouteResponse(components/media_router/browser/presentation/start_presentation_context.cc:49-59). Crucially, this path does not validate thatresult.presentation_url()is contained within the subframe’s originalpresentation_request_.presentation_urlslist. Consequently, the victim subframe’sstart()promise resolves, establishing aPresentationConnectiontargeting the attacker’s receiver.
- Origin Spoofing: The browser-drawn Global Media Controls (GMC) card origin label displays the victim subframe’s origin (
Potential Attack Scenario / Reproduction Steps
Note: These are suggested/potential steps derived from code tracing; our tooling agent does not have the capability to run code to confirm.
- An attacker hosts
https://attacker.examplewhich embeds an iframe pointing tohttps://victim.example/widgetwithallow="presentation". - The user interacts with the
victim.exampleiframe, causing it to run:new PresentationRequest(['https://victim.example/recv']).start(); - This opens the browser-drawn GMC dialog displaying a Cast card with the labeled origin of
victim.example. - While the dialog is open, the attacker top-frame executes:
navigator.presentation.defaultRequest = new PresentationRequest(['https://attacker.example/recv']); - Due to the observer bug,
MediaRouteStarter::OnDefaultPresentationChangedoverwrites its internal state with the attacker’s URL and origin. - The user clicks a Cast sink in the GMC device list.
- The browser initiates a route to the attacker’s receiver (
https://attacker.example/recv), but the route response is routed back to the subframe’s callback. - The
victim.examplesubframe receives aPresentationConnectionconnected tohttps://attacker.example/recvand may unknowingly transmit sensitive data (tokens, states) believing it is communicating with its own receiver.
Suggested Fix
There are two layers of defenses that should be applied to fully address this issue:
-
Guard state changes in
MediaRouteStarter: If theMediaRouteStarteris instantiated with an explicitstart_presentation_context_(meaning it is processing an explicit subframe request), it should ignore default presentation changes from the parent frame. For example, inOnDefaultPresentationChanged:void MediaRouteStarter::OnDefaultPresentationChanged( const content::PresentationRequest* presentation_request) { if (start_presentation_context_) { return; } ... -
Validate returned URLs in
StartPresentationContext: Ensure thatStartPresentationContext::HandleRouteResponseverifies that the returnedresult.presentation_url()is a member of its originalpresentation_request_.presentation_urlsbefore invoking the success callback (mirroring the validation performed inControllerPresentationServiceDelegateImpl::OnPresentationResponse).
Evaluated with Chrome root at commit: 87214e6721f6c34afd9181b80769a24c0c601c50
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.