CVE-2026-8007
Overview
Files Changed
components/media_router/browser/presentation/controller_presentation_service_delegate_impl.cccomponents/media_router/browser/presentation/controller_presentation_service_delegate_impl_unittest.cc
Patch
From 51be7f568ef9213f56b17fe0855fc735f677f272 Mon Sep 17 00:00:00 2001
From: mark a. foltz <mfoltz@chromium.org>
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 <muyaoxu@google.com>
Commit-Queue: Mark Foltz <mfoltz@chromium.org>
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.