CVE-2026-11241
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchrome/browser/media/router/providers/cast/cast_internal_message_util.cc |
modified | |
forchrome/browser/media/router/providers/cast/cast_internal_message_util.cc |
modified |
Files Changed
chrome/browser/media/router/providers/cast/app_activity.ccchrome/browser/media/router/providers/cast/app_activity_unittest.ccchrome/browser/media/router/providers/cast/cast_internal_message_util.ccchrome/browser/media/router/providers/cast/cast_internal_message_util_unittest.cc
Patch
From 5025f802c8f9b58ba7b51fb8c310efd7fb3bf9ed Mon Sep 17 00:00:00 2001
From: Muyao Xu <muyaoxu@google.com>
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 <jophba@chromium.org>
Reviewed-by: Jordan Bayles <jophba@chromium.org>
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"({
Regression Test / PoC
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)
Original Bug Report
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.ccchrome/browser/media/router/providers/cast/app_activity.cccomponents/media_router/common/providers/cast/channel/cast_message_handler.cccomponents/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
- Unvalidated Namespace Ingestion: In
chrome/browser/media/router/providers/cast/cast_internal_message_util.cc, the functionCastSession::Frompopulates a session’smessage_namespaces_set from the receiver’sRECEIVER_STATUSmessage (lines 413-421) without filtering for reserved namespaces usingIsCastReservedNamespace(). Additionally, the receiver-suppliedtransportIdis assigned todestination_id_(line 366) without ensuring it does not equalkPlatformReceiverId("receiver-0"). - Authorization Bypass: When a renderer sends a message via
blink::mojom::PresentationConnection, the browser callsAppActivity::SendAppMessageToReceiver(inchrome/browser/media/router/providers/cast/app_activity.cc). This function checks if the requested namespace is in thesession->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. - DCHECK-only Security Guard: The message is subsequently handled by
CastMessageHandler::SendAppMessage(incomponents/media_router/common/providers/cast/channel/cast_message_handler.cc). Line 281 contains aDCHECK(!IsCastReservedNamespace(message.namespace_())). However,DCHECKstatements are stripped in release (production) builds, leaving the browser without outbound protection for reserved namespaces. - Platform Command Injection: Because the
destination_idis set toreceiver-0, the connection is treated as akStrongvirtual 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 asLAUNCHorSTOP.
Potential Reproduction Steps
Note: These are suggested steps based on code analysis; we do not currently have a working proof-of-concept.
- 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 setstransportIdtoreceiver-0in itsRECEIVER_STATUSresponse. - Host a malicious website that uses the Presentation API to cast to this malicious receiver.
- Once the user selects the receiver in the Cast dialog, the website sends an
app_messagetargeting 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 } } } - In a release build of Chrome, the browser will forward this command to the Cast device’s platform receiver.
Suggested Fix
- In
CastSession::From(chrome/browser/media/router/providers/cast/cast_internal_message_util.cc), validate thenamespaceslist againstIsCastReservedNamespace(). Ignore or reject any reserved namespaces provided by the receiver. - In
CastSession::From, validate that thetransportIdis not equal tokPlatformReceiverId("receiver-0"). - In
CastMessageHandler::SendAppMessage(components/media_router/common/providers/cast/channel/cast_message_handler.cc), upgrade theDCHECKto aCHECKor return an errorResult::kFailedin 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.