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
Tracker497203741
Fix commit5025f802c8f9 (chromium/src) +117/-34
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
chrome/browser/media/router/providers/cast/cast_internal_message_util.cc
modified
for
chrome/browser/media/router/providers/cast/cast_internal_message_util.cc
modified

Files Changed

  • chrome/browser/media/router/providers/cast/app_activity.cc
  • chrome/browser/media/router/providers/cast/app_activity_unittest.cc
  • chrome/browser/media/router/providers/cast/cast_internal_message_util.cc
  • chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
From 5025f802c8f9b58ba7b51fb8c310efd7fb3bf9ed Mon Sep 17 00:00:00 2001
From: Muyao Xu <[email protected]>
Date: Tue, 07 Apr 2026 23:22:21 -0700
Subject: [PATCH] [Media Router] Fix message authorization bypass

This CL addresses a vulnerability where a malicious receiver could
bypass consent by spoofing transportId and reserved namespaces.

Changes:
- Filter out reserved namespaces in CastSession::From.
- Reject sessions with transportId == "receiver-0".
- Return Result::kFailed in SendAppMessage for reserved namespaces.

This CL is prepared with gemini-cli

Bug: 497203741
Change-Id: Ifd8b7d343c20e5dbbb06f3af6dff8e42dad5c2cb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718683
Commit-Queue: Jordan Bayles <[email protected]>
Reviewed-by: Jordan Bayles <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1611245}
---

diff --git a/chrome/browser/media/router/providers/cast/app_activity.cc b/chrome/browser/media/router/providers/cast/app_activity.cc
index 8e93eca..65c9fcc 100644
--- a/chrome/browser/media/router/providers/cast/app_activity.cc
+++ b/chrome/browser/media/router/providers/cast/app_activity.cc
@@ -74,12 +74,12 @@
           CastInternalMessage::ErrorCode::kSessionError,
           "Invalid session ID: " + session_id_.value_or("<missing>"));
     }
-
     return cast_channel::Result::kFailed;
   }
+
   const std::string& message_namespace = cast_message.app_message_namespace();
   if (!session->message_namespaces().contains(message_namespace)) {
-    DLOG(ERROR) << "Disallowed message namespace: " << message_namespace;
+    DVLOG(1) << "Disallowed message namespace: " << message_namespace;
     if (client && cast_message.sequence_number()) {
       client->SendErrorCodeToClient(
           *cast_message.sequence_number(),
@@ -88,6 +88,7 @@
     }
     return cast_channel::Result::kFailed;
   }
+
   return message_handler_->SendAppMessage(
       cast_channel_id(),
       cast_channel::CreateCastMessage(
diff --git a/chrome/browser/media/router/providers/cast/app_activity_unittest.cc b/chrome/browser/media/router/providers/cast/app_activity_unittest.cc
index f9f42974..f3d0a71 100644
--- a/chrome/browser/media/router/providers/cast/app_activity_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/app_activity_unittest.cc
@@ -85,7 +85,7 @@
           [](const openscreen::cast::proto::CastMessage& cast_message) {
             EXPECT_EQ("theClientId", cast_message.source_id());
             EXPECT_EQ("theTransportId", cast_message.destination_id());
-            EXPECT_EQ("urn:x-cast:com.google.foo", cast_message.namespace_());
+            EXPECT_EQ(kFakeCastNamespace, cast_message.namespace_());
             EXPECT_TRUE(cast_message.has_payload_utf8());
             EXPECT_THAT(cast_message.payload_utf8(),
                         IsJson(R"({"foo": "bar"})"));
diff --git a/chrome/browser/media/router/providers/cast/cast_internal_message_util.cc b/chrome/browser/media/router/providers/cast/cast_internal_message_util.cc
index f936b8ce..5e042d6 100644
--- a/chrome/browser/media/router/providers/cast/cast_internal_message_util.cc
+++ b/chrome/browser/media/router/providers/cast/cast_internal_message_util.cc
@@ -17,6 +17,7 @@
 #include "components/media_router/common/discovery/media_sink_internal.h"
 #include "components/media_router/common/providers/cast/cast_media_source.h"
 #include "components/media_router/common/providers/cast/channel/cast_device_capability.h"
+#include "components/media_router/common/providers/cast/channel/cast_message_util.h"
 #include "components/media_router/common/providers/cast/channel/enum_table.h"
 #include "crypto/hash.h"
 
@@ -370,6 +371,11 @@
     return nullptr;
   }
 
+  if (session->destination_id_ == cast_channel::kPlatformReceiverId) {
+    DVLOG(2) << "transportId cannot be " << cast_channel::kPlatformReceiverId;
+    return nullptr;
+  }
+
   if (session->app_id_ == kBackdropAppId) {
     DVLOG(2) << sink.sink().id() << " is running the backdrop app";
     return nullptr;
@@ -411,19 +417,27 @@
       return nullptr;
     }
   } else {
+    base::ListValue filtered_namespaces_value;
     for (const auto& namespace_value : *namespaces_value) {
       std::string message_namespace;
       if (!namespace_value.is_dict() ||
           !GetString(namespace_value.GetDict(), "name", &message_namespace)) {
         DVLOG(2) << "Missing namespace name.";
-        return nullptr;
+        continue;
+      }
+
+      if (cast_channel::IsCastReservedNamespace(message_namespace) &&
+          message_namespace != cast_channel::kMediaNamespace) {
+        DVLOG(2) << "Ignoring reserved namespace: " << message_namespace;
+        continue;
       }
 
       session->message_namespaces_.insert(std::move(message_namespace));
+      filtered_namespaces_value.Append(namespace_value.Clone());
     }
+
+    session->value_.Set("namespaces", std::move(filtered_namespaces_value));
   }
-  session->value_.Set("namespaces", namespaces_value ? namespaces_value->Clone()
-                                                     : base::ListValue());
   return session;
 }
 
diff --git a/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc b/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
index 9faafe0a..47b5e15 100644
--- a/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/json/json_reader.h"
 #include "base/test/gtest_util.h"
 #include "build/build_config.h"
+#include "chrome/browser/media/router/providers/cast/test_util.h"
 #include "chrome/browser/media/router/test/provider_test_helpers.h"
 #include "components/media_router/common/providers/cast/channel/cast_test_util.h"
 #include "components/media_router/common/test/test_helper.h"
@@ -23,7 +24,7 @@
 static constexpr char kReceiverIdToken[] = "token";
 
 base::DictValue ReceiverStatus() {
-  std::string receiver_status_str = R"({
+  constexpr char receiver_status_str[] = R"({
       "applications": [{
         "appId": "ABCDEFGH",
         "displayName": "App display name",
@@ -43,7 +44,7 @@
 // universalAppId: web receiver app ID, which is appId in ReceiverStatus without
 // universalAppId or appType
 base::DictValue ReceiverStatusWithUniversalAppId() {
-  std::string receiver_status_str = R"({
+  constexpr char receiver_status_str[] = R"({
       "applications": [{
         "appId": "AD9AF8E0",
         "displayName": "App display name",
@@ -62,15 +63,15 @@
 }
 
 void ExpectNoCastSession(const MediaSinkInternal& sink,
-                         const std::string& receiver_status_str,
-                         const std::string& reason) {
+                         std::string_view receiver_status_str,
+                         std::string_view reason) {
   auto session = CastSession::From(sink, ParseJsonDict(receiver_status_str));
   EXPECT_FALSE(session) << "Shouldn't have created session because of "
                         << reason;
 }
 
-void ExpectInvalidCastInternalMessage(const std::string& message_str,
-                                      const std::string& invalid_reason) {
+void ExpectInvalidCastInternalMessage(std::string_view message_str,
+                                      std::string_view invalid_reason) {
   EXPECT_FALSE(CastInternalMessage::From(ParseJsonDict(message_str)))
       << "message expected to be invlaid: " << invalid_reason;
 }
@@ -92,7 +93,7 @@
 #endif
 TEST_F(CastInternalMessageUtilDeathTest,
        MAYBE_CastInternalMessageFromAppMessageString) {
-  std::string message_str = R"({
+  constexpr char message_str[] = R"({
     "type": "app_message",
     "clientId": "12345",
     "sequenceNumber": 999,
@@ -108,7 +109,7 @@
   EXPECT_EQ(CastInternalMessage::Type::kAppMessage, message->type());
   EXPECT_EQ("12345", message->client_id());
   EXPECT_EQ(999, message->sequence_number());
-  EXPECT_EQ("urn:x-cast:com.google.foo", message->app_message_namespace());
+  EXPECT_EQ(kFakeCastNamespace, message->app_message_namespace());
   EXPECT_EQ("sessionId", message->session_id());
   base::DictValue message_body;
   message_body.Set("foo", base::Value("bar"));
@@ -120,7 +121,7 @@
 
 TEST_F(CastInternalMessageUtilDeathTest,
        CastInternalMessageFromV2MessageString) {
-  std::string message_str = R"({
+  constexpr char message_str[] = R"({
     "type": "v2_message",
     "clientId": "12345",
     "sequenceNumber": 999,
@@ -159,7 +160,7 @@
 #endif
 TEST_F(CastInternalMessageUtilDeathTest,
        MAYBE_CastInternalMessageFromClientConnectString) {
-  std::string message_str = R"({
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chrome/browser/media/router/providers/cast/app_activity_unittest.cc b/chrome/browser/media/router/providers/cast/app_activity_unittest.cc
index f9f42974..f3d0a71 100644
--- a/chrome/browser/media/router/providers/cast/app_activity_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/app_activity_unittest.cc
@@ -85,7 +85,7 @@
           [](const openscreen::cast::proto::CastMessage& cast_message) {
             EXPECT_EQ("theClientId", cast_message.source_id());
             EXPECT_EQ("theTransportId", cast_message.destination_id());
-            EXPECT_EQ("urn:x-cast:com.google.foo", cast_message.namespace_());
+            EXPECT_EQ(kFakeCastNamespace, cast_message.namespace_());
             EXPECT_TRUE(cast_message.has_payload_utf8());
             EXPECT_THAT(cast_message.payload_utf8(),
                         IsJson(R"({"foo": "bar"})"));
diff --git a/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc b/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
index 9faafe0a..47b5e15 100644
--- a/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
@@ -7,6 +7,7 @@
 #include "base/json/json_reader.h"
 #include "base/test/gtest_util.h"
 #include "build/build_config.h"
+#include "chrome/browser/media/router/providers/cast/test_util.h"
 #include "chrome/browser/media/router/test/provider_test_helpers.h"
 #include "components/media_router/common/providers/cast/channel/cast_test_util.h"
 #include "components/media_router/common/test/test_helper.h"
@@ -23,7 +24,7 @@
 static constexpr char kReceiverIdToken[] = "token";
 
 base::DictValue ReceiverStatus() {
-  std::string receiver_status_str = R"({
+  constexpr char receiver_status_str[] = R"({
       "applications": [{
         "appId": "ABCDEFGH",
         "displayName": "App display name",
@@ -43,7 +44,7 @@
 // universalAppId: web receiver app ID, which is appId in ReceiverStatus without
 // universalAppId or appType
 base::DictValue ReceiverStatusWithUniversalAppId() {
-  std::string receiver_status_str = R"({
+  constexpr char receiver_status_str[] = R"({
       "applications": [{
         "appId": "AD9AF8E0",
         "displayName": "App display name",
@@ -62,15 +63,15 @@
 }
 
 void ExpectNoCastSession(const MediaSinkInternal& sink,
-                         const std::string& receiver_status_str,
-                         const std::string& reason) {
+                         std::string_view receiver_status_str,
+                         std::string_view reason) {
   auto session = CastSession::From(sink, ParseJsonDict(receiver_status_str));
   EXPECT_FALSE(session) << "Shouldn't have created session because of "
                         << reason;
 }
 
-void ExpectInvalidCastInternalMessage(const std::string& message_str,
-                                      const std::string& invalid_reason) {
+void ExpectInvalidCastInternalMessage(std::string_view message_str,
+                                      std::string_view invalid_reason) {
   EXPECT_FALSE(CastInternalMessage::From(ParseJsonDict(message_str)))
       << "message expected to be invlaid: " << invalid_reason;
 }
@@ -92,7 +93,7 @@
 #endif
 TEST_F(CastInternalMessageUtilDeathTest,
        MAYBE_CastInternalMessageFromAppMessageString) {
-  std::string message_str = R"({
+  constexpr char message_str[] = R"({
     "type": "app_message",
     "clientId": "12345",
     "sequenceNumber": 999,
@@ -108,7 +109,7 @@
   EXPECT_EQ(CastInternalMessage::Type::kAppMessage, message->type());
   EXPECT_EQ("12345", message->client_id());
   EXPECT_EQ(999, message->sequence_number());
-  EXPECT_EQ("urn:x-cast:com.google.foo", message->app_message_namespace());
+  EXPECT_EQ(kFakeCastNamespace, message->app_message_namespace());
   EXPECT_EQ("sessionId", message->session_id());
   base::DictValue message_body;
   message_body.Set("foo", base::Value("bar"));
@@ -120,7 +121,7 @@
 
 TEST_F(CastInternalMessageUtilDeathTest,
        CastInternalMessageFromV2MessageString) {
-  std::string message_str = R"({
+  constexpr char message_str[] = R"({
     "type": "v2_message",
     "clientId": "12345",
     "sequenceNumber": 999,
@@ -159,7 +160,7 @@
 #endif
 TEST_F(CastInternalMessageUtilDeathTest,
        MAYBE_CastInternalMessageFromClientConnectString) {
-  std::string message_str = R"({
+  constexpr char message_str[] = R"({
       "type": "client_connect",
       "clientId": "12345",
       "message": {}
@@ -179,26 +180,26 @@
 }
 
 TEST(CastInternalMessageUtilTest, CastInternalMessageFromInvalidStrings) {
-  std::string unknown_type = R"({
+  constexpr char unknown_type[] = R"({
       "type": "some_unknown_type",
       "clientId": "12345",
       "message": {}
     })";
   ExpectInvalidCastInternalMessage(unknown_type, "unknown_type");
 
-  std::string missing_client_id = R"({
+  constexpr char missing_client_id[] = R"({
       "type": "client_connect",
       "message": {}
     })";
   ExpectInvalidCastInternalMessage(missing_client_id, "missing client ID");
 
-  std::string missing_message = R"({
+  constexpr char missing_message[] = R"({
       "type": "client_connect",
       "clientId": "12345"
     })";
   ExpectInvalidCastInternalMessage(missing_message, "missing message");
 
-  std::string app_message_missing_namespace = R"({
+  constexpr char app_message_missing_namespace[] = R"({
     "type": "app_message",
     "clientId": "12345",
     "sequenceNumber": 999,
@@ -210,7 +211,7 @@
   ExpectInvalidCastInternalMessage(app_message_missing_namespace,
                                    "missing namespace");
 
-  std::string app_message_missing_session_id = R"({
+  constexpr char app_message_missing_session_id[] = R"({
     "type": "app_message",
     "clientId": "12345",
     "sequenceNumber": 999,
@@ -222,7 +223,7 @@
   ExpectInvalidCastInternalMessage(app_message_missing_session_id,
                                    "missing session ID");
 
-  std::string app_message_missing_message = R"({
+  constexpr char app_message_missing_message[] = R"({
     "type": "app_message",
     "clientId": "12345",
     "sequenceNumber": 999,
@@ -237,7 +238,7 @@
 
 TEST(CastInternalMessageUtilTest, CastSessionFromReceiverStatusNoStatusText) {
   MediaSinkInternal sink = CreateCastSink(1);
-  std::string receiver_status_str = R"({
+  constexpr char receiver_status_str[] = R"({
       "applications": [{
         "appId": "ABCDEFGH",
         "displayName": "App display name",
@@ -255,14 +256,14 @@
   EXPECT_EQ("ABCDEFGH", session->app_id());
   EXPECT_EQ("transportId", session->destination_id());
   base::flat_set<std::string> message_namespaces = {
-      "urn:x-cast:com.google.cast.media", "urn:x-cast:com.google.foo"};
+      "urn:x-cast:com.google.cast.media", kFakeCastNamespace};
   EXPECT_EQ(message_namespaces, session->message_namespaces());
   EXPECT_EQ("App display name", session->GetRouteDescription());
 }
 
 TEST(CastInternalMessageUtilTest, CastSessionFromInvalidReceiverStatuses) {
   MediaSinkInternal sink = CreateCastSink(1);
-  std::string missing_app_id = R"({
+  constexpr char missing_app_id[] = R"({
       "applications": [{
         "displayName": "App display name",
         "namespaces": [
@@ -276,7 +277,7 @@
   })";
   ExpectNoCastSession(sink, missing_app_id, "missing app id");
 
-  std::string missing_display_name = R"({
+  constexpr char missing_display_name[] = R"({
       "applications": [{
         "appId": "ABCDEFGH",
         "namespaces": [
@@ -290,7 +291,7 @@
   })";
   ExpectNoCastSession(sink, missing_display_name, "missing display name");
 
-  std::string missing_namespaces = R"({
+  constexpr char missing_namespaces[] = R"({
       "applications": [{
         "appId": "ABCDEFGH",
         "displayName": "App display name",
@@ -302,7 +303,7 @@
   })";
   ExpectNoCastSession(sink, missing_namespaces, "missing namespaces");
 
-  std::string missing_session_id = R"({
+  constexpr char missing_session_id[] = R"({
       "applications": [{
         "appId": "ABCDEFGH",
         "displayName": "App display name",
@@ -316,7 +317,7 @@
   })";
   ExpectNoCastSession(sink, missing_session_id, "missing session id");
 
-  std::string missing_transport_id = R"({
+  constexpr char missing_transport_id[] = R"({
       "applications": [{
         "appId": "ABCDEFGH",
         "displayName": "App display name",
@@ -331,6 +332,49 @@
   ExpectNoCastSession(sink, missing_transport_id, "missing transport id");
 }
 
+TEST(CastInternalMessageUtilTest,
+     CastSessionFromReceiverStatusInvalidTransportId) {
+  MediaSinkInternal sink = CreateCastSink(1);
+  constexpr char invalid_transport_id[] = R"({
+      "applications": [{
+        "appId": "ABCDEFGH",
+        "displayName": "App display name",
+        "namespaces": [
+          {"name": "urn:x-cast:com.google.foo"}
+        ],
+        "sessionId": "sessionId",
+        "statusText":"App status",
+        "transportId":"receiver-0"
+      }]
+  })";
+  ExpectNoCastSession(sink, invalid_transport_id, "invalid transport id");
+}
+
+TEST(CastInternalMessageUtilTest,
+     CastSessionFromReceiverStatusFiltersReservedNamespaces) {
+  MediaSinkInternal sink = CreateCastSink(1);
+  constexpr char receiver_status_str[] = R"({
+      "applications": [{
+        "appId": "ABCDEFGH",
+        "displayName": "App display name",
+        "namespaces": [
+          {"name": "urn:x-cast:com.google.foo"},
+          {"name": "urn:x-cast:com.google.cast.receiver"}
+        ],
+        "sessionId": "sessionId",
+        "statusText":"App status",
+        "transportId":"transportId"
+      }]
+  })";
+  auto session = CastSession::From(sink, ParseJsonDict(receiver_status_str));
+  ASSERT_TRUE(session);
+
+  // "urn:x-cast:com.google.cast.receiver" should be filtered out.
+  base::flat_set<std::string> expected_namespaces = {
+      "urn:x-cast:com.google.foo"};
+  EXPECT_EQ(expected_namespaces, session->message_namespaces());
+}
+
 TEST(CastInternalMessageUtilTest, CreateReceiverActionCastMessage) {
   std::string client_id = "clientId";
   MediaSinkInternal sink = CreateCastSink(1);
@@ -517,7 +561,7 @@
   base::DictValue message_body;
   message_body.Set("foo", base::Value("bar"));
   openscreen::cast::proto::CastMessage cast_message =
-      cast_channel::CreateCastMessage("urn:x-cast:com.google.foo",
+      cast_channel::CreateCastMessage(kFakeCastNamespace,
                                       base::Value(std::move(message_body)),
                                       "sourceId", "transportId");
diff --git a/chrome/browser/media/router/providers/cast/cast_session_tracker_unittest.cc b/chrome/browser/media/router/providers/cast/cast_session_tracker_unittest.cc
index 43e3f6da..59cc065 100644
--- a/chrome/browser/media/router/providers/cast/cast_session_tracker_unittest.cc
+++ b/chrome/browser/media/router/providers/cast/cast_session_tracker_unittest.cc
@@ -6,6 +6,7 @@
 
 #include "base/memory/raw_ptr.h"
 #include "base/test/values_test_util.h"
+#include "chrome/browser/media/router/providers/cast/test_util.h"
 #include "chrome/browser/media/router/test/provider_test_helpers.h"
 #include "components/media_router/common/providers/cast/channel/cast_message_util.h"
 #include "components/media_router/common/providers/cast/channel/cast_test_util.h"
@@ -418,4 +419,10 @@
   }])"));
 }
 
+TEST_F(CastSessionTrackerTest, MediaNamespaceNotFiltered) {
+  AddSinkAndSendReceiverStatusResponse();
+  EXPECT_TRUE(session_->message_namespaces().contains(kMediaNamespace));
+  EXPECT_TRUE(session_->message_namespaces().contains(kFakeCastNamespace));
+}
+
 }  // namespace media_router
diff --git a/components/media_router/common/providers/cast/channel/cast_message_handler_unittest.cc b/components/media_router/common/providers/cast/channel/cast_message_handler_unittest.cc
index 9989ba4c..efa869ca 100644
--- a/components/media_router/common/providers/cast/channel/cast_message_handler_unittest.cc
+++ b/components/media_router/common/providers/cast/channel/cast_message_handler_unittest.cc
@@ -334,7 +334,7 @@
   EXPECT_GT(request_id, 0);
 
   CastMessage response;
-  response.set_namespace_("urn:x-cast:com.google.cast.receiver");
... (truncated)
Loading diff…

Original Bug Report

reported by [email protected]

Potential authorization bypass allowing web pages to send Cast platform commands

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A malicious Cast receiver can provide a spoofed transportId and reserved namespaces in its RECEIVER_STATUS message, which Chrome fails to validate. A coordinating malicious web page can then use the Presentation API to send unauthorized platform commands (e.g., LAUNCH) directly to the Cast device’s platform receiver, bypassing browser-mediated consent.

Affected files:

  • chrome/browser/media/router/providers/cast/cast_internal_message_util.cc
  • chrome/browser/media/router/providers/cast/app_activity.cc
  • components/media_router/common/providers/cast/channel/cast_message_handler.cc
  • components/media_router/common/providers/cast/channel/cast_message_util.cc

Estimated timestamp from git blame: 2022-08-25

Description

A potential security vulnerability exists in Chrome’s Cast implementation where the browser fails to properly validate namespaces and destination IDs (transportId) provided by a Cast receiver during session establishment. This allows an attacker-controlled web page, working in concert with a malicious Cast receiver, to send unauthorized platform commands directly to the Cast device’s platform receiver (receiver-0), bypassing user consent checks typically enforced by the browser.

Technical Details

  1. Unvalidated Namespace Ingestion: In chrome/browser/media/router/providers/cast/cast_internal_message_util.cc, the function CastSession::From populates a session’s message_namespaces_ set from the receiver’s RECEIVER_STATUS message (lines 413-421) without filtering for reserved namespaces using IsCastReservedNamespace(). Additionally, the receiver-supplied transportId is assigned to destination_id_ (line 366) without ensuring it does not equal kPlatformReceiverId ("receiver-0").
  2. Authorization Bypass: When a renderer sends a message via blink::mojom::PresentationConnection, the browser calls AppActivity::SendAppMessageToReceiver (in chrome/browser/media/router/providers/cast/app_activity.cc). This function checks if the requested namespace is in the session->message_namespaces() set. Because this set was populated using the unvalidated receiver-supplied data, a renderer can specify a reserved namespace (e.g., urn:x-cast:com.google.cast.receiver) and pass this check.
  3. DCHECK-only Security Guard: The message is subsequently handled by CastMessageHandler::SendAppMessage (in components/media_router/common/providers/cast/channel/cast_message_handler.cc). Line 281 contains a DCHECK(!IsCastReservedNamespace(message.namespace_())). However, DCHECK statements are stripped in release (production) builds, leaving the browser without outbound protection for reserved namespaces.
  4. Platform Command Injection: Because the destination_id is set to receiver-0, the connection is treated as a kStrong virtual connection to the platform receiver. The renderer-supplied JSON body is serialized and sent to the Cast device, allowing an attacker to inject forged platform commands, such as LAUNCH or STOP.

Potential Reproduction Steps

Note: These are suggested steps based on code analysis; we do not currently have a working proof-of-concept.

  1. Set up a malicious Cast receiver application that includes a reserved namespace (e.g., urn:x-cast:com.google.cast.receiver) in its namespaces and sets transportId to receiver-0 in its RECEIVER_STATUS response.
  2. Host a malicious website that uses the Presentation API to cast to this malicious receiver.
  3. Once the user selects the receiver in the Cast dialog, the website sends an app_message targeting the reserved namespace:
    {
      type: 'app_message',
      message: {
        sessionId: '<assigned_session_id>',
        namespaceName: 'urn:x-cast:com.google.cast.receiver',
        message: { type: 'LAUNCH', appId: '<arbitrary_app_id>', requestId: 1 }
      }
    }
    
  4. In a release build of Chrome, the browser will forward this command to the Cast device’s platform receiver.

Suggested Fix

  1. In CastSession::From (chrome/browser/media/router/providers/cast/cast_internal_message_util.cc), validate the namespaces list against IsCastReservedNamespace(). Ignore or reject any reserved namespaces provided by the receiver.
  2. In CastSession::From, validate that the transportId is not equal to kPlatformReceiverId ("receiver-0").
  3. In CastMessageHandler::SendAppMessage (components/media_router/common/providers/cast/channel/cast_message_handler.cc), upgrade the DCHECK to a CHECK or return an error Result::kFailed in release builds if a reserved namespace is encountered.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from 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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker