CVE-2026-8007
Overview
Background
- Presentation API
- a web API, backed by Media Router, that lets a page start or reconnect to a “presentation” (e.g. casting) identified by a presentation URL and a presentation ID.
- `ReconnectPresentation`
- the
ControllerPresentationServiceDelegateImplentry point that rejoins an existing presentation, invoked on behalf of a renderer. - `IsValidPresentationUrl`
- the Media Router helper that validates a presentation URL’s scheme, already used by the
StartPresentationpath. - Media-source URN
- a
urn:x-org.chromium.media:source:...identifier that can name capture sources such asdesktop:screen:0:0; a presentation ID ofauto-joinlets a request attach to a session without the picker UI.
Root Cause Analysis
ControllerPresentationServiceDelegateImpl::ReconnectPresentation consumed the renderer-supplied presentation_urls without validating their scheme, unlike the sibling StartPresentation path which already called IsValidPresentationUrl. A compromised renderer could therefore pass a media-source URN naming a desktop/screen capture source and, combined with the auto-join presentation ID, drive the reconnect flow into a capture session instead of a legitimate presentation — violating the invariant that only supported presentation URL schemes reach the routing logic.
The fix adds if (!std::ranges::all_of(presentation_urls, IsValidPresentationUrl)), which runs the error callback with PresentationError(PresentationErrorType::NO_PRESENTATION_FOUND, "Invalid presentation URL.") and returns before any local_presentation_manager lookup or route join. The shipped regression test ReconnectPresentationWithInvalidUrl confirms that for the URN urn:x-org.chromium.media:source:desktop:screen:0:0 with ID auto-join, JoinRouteInternal is never called and the error callback fires. This is a tightening that brings ReconnectPresentation to parity with StartPresentation.
ReconnectPresentation trusted renderer-supplied URLs that StartPresentation already validated; the fix adds the same IsValidPresentationUrl scheme check so malicious capture-source URNs are rejected before routing.Attack Path
- Compromise a renderer the attacker controls a renderer with an active tab-mirroring (presentation) session.
- Craft a malicious URN
build a presentation request whose
presentation_urlscontainsurn:x-org.chromium.media:source:desktop:screen:0:0, a capture source rather than a valid presentation URL. - Call `ReconnectPresentation` with `auto-join`
use the
auto-joinpresentation ID so the reconnect attaches without surfacing the desktop picker UI. - Bypass the picker and escalate capture
pre-patch the unvalidated URN reached the routing/join path, which the advisory describes as escalating a tab-mirroring session into full-screen/desktop capture without user approval; the post-patch guard returns
NO_PRESENTATION_FOUNDfirst, as the regression test verifies.
Impact Assessment
ReconnectPresentation, bypassing the desktop picker; the precondition is an already-compromised renderer and (per the advisory) an active mirroring session. The advisory rates this low severity and classifies it as insufficient validation of untrusted input in Cast. No in-the-wild exploitation is indicated, and the capture escalation is grounded in the advisory and the regression test rather than inferred.Files Changed
components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cccomponents/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
Audit Directions
- Validation parity across sibling entry pointswhen one API path (
StartPresentation) validates input, audit every other path that consumes the same parameter (ReconnectPresentation, join/close variants) for the identical check. - URN/scheme allowlisting before routingsearch Media Router and other IPC-reachable surfaces for places that accept renderer-supplied URLs/URNs and route them without an explicit scheme allowlist, especially where capture-source URNs are meaningful.
- `auto-join` / picker-bypass flowsreview code paths reachable with the
auto-joinpresentation ID, which skip user-facing pickers and therefore depend entirely on upstream input validation.
Patch
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
Regression Test / PoC
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
Original Bug Report
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.cccontent/browser/presentation/presentation_service_impl.ccchrome/browser/media/router/mojo/media_router_desktop.cccomponents/media_router/common/media_source.ccchrome/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
- The attacker must compromise a renderer process.
- 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.
- The user visits an attacker-controlled page and initiates Cast tab-mirroring to a local device.
- The attacker exploits a separate bug (e.g., in V8) to compromise the renderer process.
- The compromised renderer constructs a malicious presentation URL for full-screen capture:
urn:x-org.chromium.media:source:desktop:screen:0:0. - The renderer bypasses Blink’s JavaScript validation and directly sends a
blink.mojom.PresentationService.ReconnectPresentationMojo IPC message to the Browser process. The message uses the malicious URN and setspresentation_id="auto-join". - In the Browser process,
ControllerPresentationServiceDelegateImpl::ReconnectPresentationreceives the request. Lacking validation, it passes the URN toMediaRouterDesktop::JoinRoute. - The request routes to
CastActivityManager::JoinSession. Becausepresentation_idisauto-joinand the default action policy for the newly created desktop source iskCreateSession, it looks up the active sink for the tab. CastActivityManagerseamlessly terminates the tab-mirroring session and callsLaunchSessionto start a new full-screen capture session to the same sink using the malicious URN.MirroringActivityparses the stream ID (screen:0:0), initializes aDesktopCaptureDevice, 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.