Low chrome Logic Error 🔧 Commit mapped

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
Tracker513745699
Fix commitaac0c8f0f816 (chromium/src) +121/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chrome/browser/media/router/mojo/media_router_desktop.cc
modified
TEST_F
chrome/browser/media/router/mojo/media_router_desktop_unittest.cc
modified

Files Changed

  • chrome/browser/media/router/mojo/media_router_desktop.cc
  • chrome/browser/media/router/mojo/media_router_desktop.h
  • chrome/browser/media/router/mojo/media_router_desktop_unittest.cc
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.