Chrome · Cast
CVE-2026-14115
Logic Error in Cast
Overview
Low
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/media/router/mojo/media_router_desktop.cc |
modified | |
TEST_Fchrome/browser/media/router/mojo/media_router_desktop_unittest.cc |
modified |
Files Changed
chrome/browser/media/router/mojo/media_router_desktop.ccchrome/browser/media/router/mojo/media_router_desktop.hchrome/browser/media/router/mojo/media_router_desktop_unittest.cc
Patch
From aac0c8f0f81629b1546fd108a045a321d6103bb6 Mon Sep 17 00:00:00 2001 From: Muyao Xu <[email protected]> Date: Wed, 27 May 2026 14:10:57 -0700 Subject: [PATCH] [MediaRouter] Prevent silent escalation to desktop capture in JoinRoute This CL adds a check in `MediaRouterDesktop::JoinRoute` to prevent joining a route with a desktop capture source unless the existing route is also desktop capture or it's not an auto-join attempt. This prevents silent escalation from tab to desktop capture without user consent. Bug: 513745699 Change-Id: If0949b517e4e83a9c7053e9ed8e385a4d3121034 Fixed: 513745699 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7870210 Reviewed-by: Ahmed Moussa <[email protected]> Commit-Queue: Muyao Xu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1637223} --- diff --git a/chrome/browser/media/router/mojo/media_router_desktop.cc b/chrome/browser/media/router/mojo/media_router_desktop.cc index 7518ac71..cd68027 100644 --- a/chrome/browser/media/router/mojo/media_router_desktop.cc +++ b/chrome/browser/media/router/mojo/media_router_desktop.cc @@ -229,6 +229,7 @@ MediaRouteResponseCallback callback, base::TimeDelta timeout) { DCHECK_CURRENTLY_ON(content::BrowserThread::UI); + std::optional<mojom::MediaRouteProviderId> provider_id = GetProviderIdForPresentation(presentation_id); if (!provider_id || !HasJoinableRoute()) { @@ -241,6 +242,16 @@ return; } + if (IsDesktopCaptureEscalation(MediaSource(source_id), presentation_id)) { + std::unique_ptr<RouteRequestResult> result = RouteRequestResult::FromError( + "Cannot switch to desktop capture without user consent", + mojom::RouteRequestResultCode::USER_NOT_ALLOWED); + MediaRouterMetrics::RecordJoinRouteResultCode(result->result_code(), + provider_id); + std::move(callback).Run(nullptr, *result); + return; + } + const content::FrameTreeNodeId frame_tree_node_id = web_contents ? web_contents->GetPrimaryMainFrame()->GetFrameTreeNodeId() : content::FrameTreeNodeId(); @@ -1001,6 +1012,35 @@ return !(current_routes_.empty()); } +bool MediaRouterDesktop::IsDesktopCaptureEscalation( + const MediaSource& new_source, + const std::string& presentation_id) const { + if (!new_source.IsDesktopMirroringSource()) { + return false; + } + + if (presentation_id == kAutoJoinPresentationId) { + // Auto-join is not supported for desktop mirroring to prevent silent + // escalation from non-desktop to desktop capture without user consent. + return true; + } + + auto current_routes = GetCurrentRoutes(); + auto it = std::ranges::find_if(current_routes, [&](const MediaRoute& route) { + return route.presentation_id() == presentation_id; + }); + if (it != current_routes.end()) { + // Block joining if the target route is not also desktop mirroring. + // This prevents upgrading to desktop capture without explicit user consent. + return !it->media_source().IsDesktopMirroringSource(); + } + + // Execution falls through here if a non-existent Cast route ID bypasses + // prior checks. Returning true treats this as an escalation to block the + // request. + return true; +} + bool MediaRouterDesktop::ShouldInitializeMediaRouteProviders() const { return !(disable_media_route_providers_for_test_ || base::CommandLine::ForCurrentProcess()->HasSwitch( diff --git a/chrome/browser/media/router/mojo/media_router_desktop.h b/chrome/browser/media/router/mojo/media_router_desktop.h index 978f06a..e6dcafb 100644 --- a/chrome/browser/media/router/mojo/media_router_desktop.h +++ b/chrome/browser/media/router/mojo/media_router_desktop.h @@ -279,6 +279,11 @@ // JoinRoute(). bool HasJoinableRoute() const; + // Returns true if the join request should be blocked because it attempts to + // upgrade a non-desktop session to a desktop session. + bool IsDesktopCaptureEscalation(const MediaSource& new_source, + const std::string& presentation_id) const; + // Returns true if the default MRPs should be initialized. bool ShouldInitializeMediaRouteProviders() const; @@ -293,7 +298,6 @@ friend class MediaRouterMojoTest; friend class MediaRouterIntegrationBrowserTest; friend class MediaRouterNativeIntegrationBrowserTest; - FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, JoinRouteTimedOutFails); FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, HandleIssue); FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, HandlePermissionIssue); FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, @@ -306,7 +310,6 @@ FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, CreateRouteFails); FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, CreateRouteIncognitoMismatchFails); - FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, JoinRouteNotFoundFails); FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, TerminateRouteFails); FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, GetMediaController); FRIEND_TEST_ALL_PREFIXES(MediaRouterDesktopTest, diff --git a/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc b/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc index 98310ac2..0532215 100644 --- a/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc +++ b/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc @@ -177,6 +177,16 @@ result_code, expected_count); } + void JoinRoute(const MediaSource::Id& source_id, + const std::string& presentation_id, + const url::Origin& origin, + content::WebContents* web_contents, + MediaRouteResponseCallback callback, + base::TimeDelta timeout) { + router()->JoinRoute(source_id, presentation_id, origin, web_contents, + std::move(callback), timeout); + } + std::unique_ptr<MediaRouterDesktop> CreateMediaRouter() override { auto router = std::unique_ptr<MediaRouterDesktop>( new StubMediaRouterDesktop(profile())); @@ -297,11 +307,11 @@ DoInvoke(nullptr, "", "Route not found", mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, _)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); - router()->JoinRoute(kSource, kPresentationId, - url::Origin::Create(GURL(kOrigin)), nullptr, - base::BindOnce(&RouteResponseCallbackHandler::Invoke, - base::Unretained(&handler)), - base::Milliseconds(kTimeoutMillis)); + JoinRoute(kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)), + nullptr, + base::BindOnce(&RouteResponseCallbackHandler::Invoke, + base::Unretained(&handler)), + base::Milliseconds(kTimeoutMillis)); run_loop.Run(); ExpectResultBucketCount("JoinRoute", mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, 1); @@ -312,7 +322,6 @@ // is a route to join. const std::vector<MediaRoute> routes{CreateMediaRoute()}; UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes); - EXPECT_TRUE(router()->HasJoinableRoute()); EXPECT_CALL(mock_cast_provider_, JoinRouteInternal( @@ -329,16 +338,72 @@ EXPECT_CALL(handler, DoInvoke(nullptr, "", kError, mojom::RouteRequestResultCode::TIMED_OUT, _)) .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); })); - router()->JoinRoute(kSource, kPresentationId, - url::Origin::Create(GURL(kOrigin)), nullptr, - base::BindOnce(&RouteResponseCallbackHandler::Invoke, - base::Unretained(&handler)), - base::Milliseconds(kTimeoutMillis)); + JoinRoute(kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)), + nullptr, + base::BindOnce(&RouteResponseCallbackHandler::Invoke, + base::Unretained(&handler)), + base::Milliseconds(kTimeoutMillis)); run_loop.Run(); ExpectCastResultBucketCount("JoinRoute", mojom::RouteRequestResultCode::TIMED_OUT, 1); } +TEST_F(MediaRouterDesktopTest, JoinRoute_EscalationFromTabToDesktopFails) { + MediaRoute route(kRouteId, MediaSource(kTabSourceOne), kSinkId, kDescription, + true); + route.set_presentation_id(kPresentationId); + route.set_controller_type(RouteControllerType::kGeneric); + + const std::vector<MediaRoute> routes{route}; + UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes); + + const std::string desktop_source = + "urn:x-org.chromium.media:source:desktop:screen:0:0"; + RouteResponseCallbackHandler handler; + base::RunLoop run_loop; + + EXPECT_CALL(handler, + DoInvoke(nullptr, "", + "Cannot switch to desktop capture without user consent", + mojom::RouteRequestResultCode::USER_NOT_ALLOWED, _)) + .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc b/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc
index 98310ac2..0532215 100644
--- a/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc
+++ b/chrome/browser/media/router/mojo/media_router_desktop_unittest.cc
@@ -177,6 +177,16 @@
result_code, expected_count);
}
+ void JoinRoute(const MediaSource::Id& source_id,
+ const std::string& presentation_id,
+ const url::Origin& origin,
+ content::WebContents* web_contents,
+ MediaRouteResponseCallback callback,
+ base::TimeDelta timeout) {
+ router()->JoinRoute(source_id, presentation_id, origin, web_contents,
+ std::move(callback), timeout);
+ }
+
std::unique_ptr<MediaRouterDesktop> CreateMediaRouter() override {
auto router = std::unique_ptr<MediaRouterDesktop>(
new StubMediaRouterDesktop(profile()));
@@ -297,11 +307,11 @@
DoInvoke(nullptr, "", "Route not found",
mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, _))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
- router()->JoinRoute(kSource, kPresentationId,
- url::Origin::Create(GURL(kOrigin)), nullptr,
- base::BindOnce(&RouteResponseCallbackHandler::Invoke,
- base::Unretained(&handler)),
- base::Milliseconds(kTimeoutMillis));
+ JoinRoute(kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)),
+ nullptr,
+ base::BindOnce(&RouteResponseCallbackHandler::Invoke,
+ base::Unretained(&handler)),
+ base::Milliseconds(kTimeoutMillis));
run_loop.Run();
ExpectResultBucketCount("JoinRoute",
mojom::RouteRequestResultCode::ROUTE_NOT_FOUND, 1);
@@ -312,7 +322,6 @@
// is a route to join.
const std::vector<MediaRoute> routes{CreateMediaRoute()};
UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes);
- EXPECT_TRUE(router()->HasJoinableRoute());
EXPECT_CALL(mock_cast_provider_,
JoinRouteInternal(
@@ -329,16 +338,72 @@
EXPECT_CALL(handler, DoInvoke(nullptr, "", kError,
mojom::RouteRequestResultCode::TIMED_OUT, _))
.WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
- router()->JoinRoute(kSource, kPresentationId,
- url::Origin::Create(GURL(kOrigin)), nullptr,
- base::BindOnce(&RouteResponseCallbackHandler::Invoke,
- base::Unretained(&handler)),
- base::Milliseconds(kTimeoutMillis));
+ JoinRoute(kSource, kPresentationId, url::Origin::Create(GURL(kOrigin)),
+ nullptr,
+ base::BindOnce(&RouteResponseCallbackHandler::Invoke,
+ base::Unretained(&handler)),
+ base::Milliseconds(kTimeoutMillis));
run_loop.Run();
ExpectCastResultBucketCount("JoinRoute",
mojom::RouteRequestResultCode::TIMED_OUT, 1);
}
+TEST_F(MediaRouterDesktopTest, JoinRoute_EscalationFromTabToDesktopFails) {
+ MediaRoute route(kRouteId, MediaSource(kTabSourceOne), kSinkId, kDescription,
+ true);
+ route.set_presentation_id(kPresentationId);
+ route.set_controller_type(RouteControllerType::kGeneric);
+
+ const std::vector<MediaRoute> routes{route};
+ UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes);
+
+ const std::string desktop_source =
+ "urn:x-org.chromium.media:source:desktop:screen:0:0";
+ RouteResponseCallbackHandler handler;
+ base::RunLoop run_loop;
+
+ EXPECT_CALL(handler,
+ DoInvoke(nullptr, "",
+ "Cannot switch to desktop capture without user consent",
+ mojom::RouteRequestResultCode::USER_NOT_ALLOWED, _))
+ .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
+
+ JoinRoute(desktop_source, kPresentationId, url::Origin::Create(GURL(kOrigin)),
+ nullptr,
+ base::BindOnce(&RouteResponseCallbackHandler::Invoke,
+ base::Unretained(&handler)),
+ base::Milliseconds(kTimeoutMillis));
+ run_loop.Run();
+}
+
+TEST_F(MediaRouterDesktopTest, JoinRoute_AutoJoinEscalationFails) {
+ MediaRoute route(kRouteId, MediaSource(kTabSourceOne), kSinkId, kDescription,
+ true);
+ route.set_presentation_id(kPresentationId);
+ route.set_controller_type(RouteControllerType::kGeneric);
+
+ const std::vector<MediaRoute> routes{route};
+ UpdateRoutes(mojom::MediaRouteProviderId::CAST, routes);
+
+ const std::string desktop_source =
+ "urn:x-org.chromium.media:source:desktop:screen:0:0";
+ RouteResponseCallbackHandler handler;
+ base::RunLoop run_loop;
+
+ EXPECT_CALL(handler,
+ DoInvoke(nullptr, "",
+ "Cannot switch to desktop capture without user consent",
+ mojom::RouteRequestResultCode::USER_NOT_ALLOWED, _))
+ .WillOnce(InvokeWithoutArgs([&run_loop]() { run_loop.Quit(); }));
+
+ JoinRoute(desktop_source, kAutoJoinPresentationId,
+ url::Origin::Create(GURL(kOrigin)), nullptr,
+ base::BindOnce(&RouteResponseCallbackHandler::Invoke,
+ base::Unretained(&handler)),
+ base::Milliseconds(kTimeoutMillis));
+ run_loop.Run();
+}
+
TEST_F(MediaRouterDesktopTest, DetachRoute) {
TestDetachRoute();
}
Loading diff…
Original Bug Report
The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.
References
On This Page