Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Media Router
DescriptionInappropriate implementation in Media Router
ComponentMedia Router
Bug ClassLogic Error
Tracker519348818
Fix commit52a0d0f81737 (chromium/src) +38/-1
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
for
chrome/browser/ui/global_media_controls/test_helper.cc
modified
if
chrome/browser/ui/media_router/media_route_starter.cc
modified

Files Changed

  • chrome/browser/ui/global_media_controls/test_helper.cc
  • chrome/browser/ui/global_media_controls/test_helper.h
  • chrome/browser/ui/media_router/media_route_starter.cc
  • chrome/browser/ui/media_router/media_route_starter_unittest.cc
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
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.cc
  • components/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:

  1. Unconditional Observer Registration: The constructor of MediaRouteStarter registers 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 explicit start_presentation_context_:

    // chrome/browser/ui/media_router/media_route_starter.cc:80-82
    if (presentation_manager_) {
      presentation_manager_->AddObserver(this);
    }
    
  2. State Poisoning via Default Request Updates: If the top-frame changes its default request (by setting navigator.presentation.defaultRequest without requiring a user gesture) while the Cast dialog is open, MediaRouteStarter::OnDefaultPresentationChanged is called. It lacks any check for whether start_presentation_context_ is active and immediately overwrites presentation_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);
      }
    }
    
  3. 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 when OnDefaultPresentationChanged fires. 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 that result.presentation_url() is contained within the subframe’s original presentation_request_.presentation_urls list. Consequently, the victim subframe’s start() promise resolves, establishing a PresentationConnection targeting the attacker’s receiver.

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.

  1. An attacker hosts https://attacker.example which embeds an iframe pointing to https://victim.example/widget with allow="presentation".
  2. The user interacts with the victim.example iframe, causing it to run:
    new PresentationRequest(['https://victim.example/recv']).start();
    
  3. This opens the browser-drawn GMC dialog displaying a Cast card with the labeled origin of victim.example.
  4. While the dialog is open, the attacker top-frame executes:
    navigator.presentation.defaultRequest = new PresentationRequest(['https://attacker.example/recv']);
    
  5. Due to the observer bug, MediaRouteStarter::OnDefaultPresentationChanged overwrites its internal state with the attacker’s URL and origin.
  6. The user clicks a Cast sink in the GMC device list.
  7. 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.
  8. The victim.example subframe receives a PresentationConnection connected to https://attacker.example/recv and 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:

  1. Guard state changes in MediaRouteStarter: If the MediaRouteStarter is instantiated with an explicit start_presentation_context_ (meaning it is processing an explicit subframe request), it should ignore default presentation changes from the parent frame. For example, in OnDefaultPresentationChanged:

    void MediaRouteStarter::OnDefaultPresentationChanged(
        const content::PresentationRequest* presentation_request) {
      if (start_presentation_context_) {
        return;
      }
      ...
    
  2. Validate returned URLs in StartPresentationContext: Ensure that StartPresentationContext::HandleRouteResponse verifies that the returned result.presentation_url() is a member of its original presentation_request_.presentation_urls before invoking the success callback (mirroring the validation performed in ControllerPresentationServiceDelegateImpl::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.

View on issue tracker