Low CVSS 7.5 chrome Input Validation 🔧 Commit mapped

Overview

Low
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Cast
DescriptionInsufficient validation of untrusted input in Cast
ComponentCast
Sub-componentMediaRouter
Bug ClassInput Validation
Tracker513745699
Fix commitaac0c8f0f816 (chromium/src) +121/-13
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-05-16 (per vendor advisory)
Fix landed2026-05-27 (public git)
Disclosed2026-06-30
Patch gap34 days — fix public in source before the advisory

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.