Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Cast
DescriptionInsufficient validation of untrusted input in Cast
ComponentCast
Bug ClassLogic Error
Tracker496399759
Fix commit51be7f568ef9 (chromium/src) +39/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Files Changed

  • components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc
  • components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
From 51be7f568ef9213f56b17fe0855fc735f677f272 Mon Sep 17 00:00:00 2001
From: mark a. foltz <[email protected]>
Date: Tue, 31 Mar 2026 14:44:07 -0700
Subject: [PATCH] [Media Router] Add URL validation to ReconnectPresentation.

This change adds a call to IsValidPresentationUrl in
ControllerPresentationServiceDelegateImpl::ReconnectPresentation to
ensure that only supported URL schemes are processed.

Previously, ReconnectPresentation lacked validation for the
presentation_urls parameter, allowing a compromised renderer to initiate
unauthorized desktop or cross-tab capture by sending a malicious URN
(e.g., 'urn:x-org.chromium.media:source:desktop:screen:0:0'). By
injecting such a URN with a presentation ID of 'auto-join', an attacker
could bypass the desktop picker UI and escalate an active tab-mirroring
session into a full-screen capture session.

This fix mirrors the validation logic already present in
StartPresentation.

AI disclosure: Prepared with gemini_cli

Fixed: 496399759
Change-Id: I5be5a81b35a0d1fd41de33adecac70bb0e7545c3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718183
Reviewed-by: Muyao Xu <[email protected]>
Commit-Queue: Mark Foltz <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1608112}
---

diff --git a/components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc b/components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc
index 3cf3b468..7107be1 100644
--- a/components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc
+++ b/components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc
@@ -510,6 +510,13 @@
     return;
   }
 
+  if (!std::ranges::all_of(presentation_urls, IsValidPresentationUrl)) {
+    std::move(error_cb).Run(
+        PresentationError(PresentationErrorType::NO_PRESENTATION_FOUND,
+                          "Invalid presentation URL."));
+    return;
+  }
+
   auto* local_presentation_manager =
       LocalPresentationManagerFactory::GetOrCreateForWebContents(
           &GetWebContents());
diff --git a/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc b/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
index 866b0d45..bf93ee2e 100644
--- a/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
+++ b/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
@@ -805,6 +805,38 @@
           &MockCreatePresentationConnectionCallbacks::OnCreateConnectionError,
           base::Unretained(&mock_create_connection_callbacks)));
 }
+
+TEST_F(ControllerPresentationServiceDelegateImplTest,
+       ReconnectPresentationWithInvalidUrl) {
+  content::WebContentsTester::For(GetWebContents())
+      ->NavigateAndCommit(GURL(kFrameUrl));
+
+  MockCreatePresentationConnectionCallbacks mock_create_connection_callbacks;
+  const std::string kPresentationId("auto-join");
+
+  // A URN that should be blocked.
+  const GURL invalid_url("urn:x-org.chromium.media:source:desktop:screen:0:0");
+  content::PresentationRequest invalid_request(
+      {main_frame_process_id_, main_frame_routing_id_}, {invalid_url},
+      frame_origin_);
+
+  // JoinRouteInternal should NOT be called.
+  EXPECT_CALL(*router_, JoinRouteInternal(_, _, _, _, _, _)).Times(0);
+
+  // Error callback should be called.
+  EXPECT_CALL(mock_create_connection_callbacks, OnCreateConnectionError(_))
+      .Times(1);
+
+  delegate_impl_->ReconnectPresentation(
+      invalid_request, kPresentationId,
+      base::BindOnce(
+          &MockCreatePresentationConnectionCallbacks::OnCreateConnectionSuccess,
+          base::Unretained(&mock_create_connection_callbacks)),
+      base::BindOnce(
+          &MockCreatePresentationConnectionCallbacks::OnCreateConnectionError,
+          base::Unretained(&mock_create_connection_callbacks)));
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 }  // namespace media_router
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc b/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
index 866b0d45..bf93ee2e 100644
--- a/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
+++ b/components/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
@@ -805,6 +805,38 @@
           &MockCreatePresentationConnectionCallbacks::OnCreateConnectionError,
           base::Unretained(&mock_create_connection_callbacks)));
 }
+
+TEST_F(ControllerPresentationServiceDelegateImplTest,
+       ReconnectPresentationWithInvalidUrl) {
+  content::WebContentsTester::For(GetWebContents())
+      ->NavigateAndCommit(GURL(kFrameUrl));
+
+  MockCreatePresentationConnectionCallbacks mock_create_connection_callbacks;
+  const std::string kPresentationId("auto-join");
+
+  // A URN that should be blocked.
+  const GURL invalid_url("urn:x-org.chromium.media:source:desktop:screen:0:0");
+  content::PresentationRequest invalid_request(
+      {main_frame_process_id_, main_frame_routing_id_}, {invalid_url},
+      frame_origin_);
+
+  // JoinRouteInternal should NOT be called.
+  EXPECT_CALL(*router_, JoinRouteInternal(_, _, _, _, _, _)).Times(0);
+
+  // Error callback should be called.
+  EXPECT_CALL(mock_create_connection_callbacks, OnCreateConnectionError(_))
+      .Times(1);
+
+  delegate_impl_->ReconnectPresentation(
+      invalid_request, kPresentationId,
+      base::BindOnce(
+          &MockCreatePresentationConnectionCallbacks::OnCreateConnectionSuccess,
+          base::Unretained(&mock_create_connection_callbacks)),
+      base::BindOnce(
+          &MockCreatePresentationConnectionCallbacks::OnCreateConnectionError,
+          base::Unretained(&mock_create_connection_callbacks)));
+}
+
 #endif  // !BUILDFLAG(IS_ANDROID)
 
 }  // namespace media_router
Loading diff…

Original Bug Report

reported by [email protected]

Unauthorized desktop capture via missing URL validation in ReconnectPresentation

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can initiate unauthorized desktop or cross-tab capture by sending a malicious URN to ControllerPresentationServiceDelegateImpl::ReconnectPresentation. Because this method lacks URL validation, it allows the attacker to silently escalate an active tab-mirroring session into a full-screen capture session. This bypasses the user-consent UI (desktop_picker_->Show()) normally required for such capture.

Affected files:

  • components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc
  • content/browser/presentation/presentation_service_impl.cc
  • chrome/browser/media/router/mojo/media_router_desktop.cc
  • components/media_router/common/media_source.cc
  • chrome/browser/media/router/providers/cast/cast_activity_manager.cc

Estimated timestamp from git blame: 2024-11-06

Description

A potential vulnerability exists in the implementation of ReconnectPresentation that allows a compromised renderer to initiate unauthorized desktop or cross-tab capture without user consent via the desktop picker UI.

The issue resides in ControllerPresentationServiceDelegateImpl::ReconnectPresentation (in components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc). Unlike StartPresentation, which correctly calls IsValidPresentationUrl to enforce protocol restrictions (e.g., blocking urn: schemes), ReconnectPresentation fails to validate the presentation_urls parameter.

Because GURL accepts urn: as a valid scheme and MediaSource stores the URL spec directly as its ID, an attacker can inject a malicious URN such as urn:x-org.chromium.media:source:desktop:screen:0:0 (for full-screen capture) or urn:x-org.chromium.media:source:desktop:web-contents-media-stream://<pid>:<fid> (for cross-tab capture).

When a ReconnectPresentation request is sent with presentation_id="auto-join" and a malicious URN, MediaRouterDesktop::JoinRoute processes it. If the user is already mirroring a tab to a Cast device, the CAST provider reuses the existing sink and launches a new mirroring activity via CastActivityManager::JoinSession. The MirroringActivity then extracts the attacker-controlled DesktopStreamId from the URN and passes it to the capture service.

Crucially, this JoinRoute path bypasses the desktop_picker_->Show() call, which is the normal user-consent gate for desktop capture and is only present in the CreateRoute path.

Potential Impact

This represents a privilege escalation from tab-mirroring to full-screen or cross-tab capture. While the captured stream is sent to the local Cast sink (e.g., a TV), it constitutes an unauthorized capture of sensitive information from the user’s desktop or other open tabs without their knowledge or the required picker UI prompt.

Preconditions

  1. The attacker must compromise a renderer process.
  2. The user must be actively Cast-mirroring a tab from that compromised renderer.

Suggested Reproduction Steps (Conceptual)

Note: These are potential steps as the AI agent cannot execute code to verify a working PoC.

  1. The user visits an attacker-controlled page and initiates Cast tab-mirroring to a local device.
  2. The attacker exploits a separate bug (e.g., in V8) to compromise the renderer process.
  3. The compromised renderer constructs a malicious presentation URL for full-screen capture: urn:x-org.chromium.media:source:desktop:screen:0:0.
  4. The renderer bypasses Blink’s JavaScript validation and directly sends a blink.mojom.PresentationService.ReconnectPresentation Mojo IPC message to the Browser process. The message uses the malicious URN and sets presentation_id="auto-join".
  5. In the Browser process, ControllerPresentationServiceDelegateImpl::ReconnectPresentation receives the request. Lacking validation, it passes the URN to MediaRouterDesktop::JoinRoute.
  6. The request routes to CastActivityManager::JoinSession. Because presentation_id is auto-join and the default action policy for the newly created desktop source is kCreateSession, it looks up the active sink for the tab.
  7. CastActivityManager seamlessly terminates the tab-mirroring session and calls LaunchSession to start a new full-screen capture session to the same sink using the malicious URN.
  8. MirroringActivity parses the stream ID (screen:0:0), initializes a DesktopCaptureDevice, and begins streaming the user’s entire screen without ever showing the desktop picker UI.

Suggested Fix

Add a call to IsValidPresentationUrl() within ControllerPresentationServiceDelegateImpl::ReconnectPresentation (in components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cc) to validate the provided presentation_urls before processing them, mirroring the logic used in StartPresentation:

  if (presentation_urls.empty()) {
    // ... error handling ...
    return;
  }
  if (!std::ranges::all_of(presentation_urls, IsValidPresentationUrl)) {
    std::move(error_cb).Run(
        PresentationError(PresentationErrorType::NO_PRESENTATION_FOUND,
                          "Invalid presentation URL."));
    return;
  }

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker