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 <muyaoxu@google.com>
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 <ahmedmoussa@google.com>
Commit-Queue: Muyao Xu <muyaoxu@google.com>
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