Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Chromecast
DescriptionInsufficient validation of untrusted input in Chromecast
ComponentChromecast
Bug ClassLogic Error
Tracker517651910
Fix commit980e128c4d30 (chromium/src) +92/-5
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
for
chromecast/browser/cast_web_service.cc
modified
if
chromecast/browser/cast_web_service.cc
modified
for
chromecast/browser/webui/cast_webui_controller_factory.cc
modified
CastWebUiControllerFactoryTest
chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
modified
CastWebUiControllerFactoryTest
chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
modified
TEST_F
chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
modified

Files Changed

  • chromecast/browser/BUILD.gn
  • chromecast/browser/cast_web_service.cc
  • chromecast/browser/cast_web_service.h
  • chromecast/browser/webui/cast_webui_controller_factory.cc
  • chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
From 980e128c4d30506e87d586a9ea696423a0ab765a Mon Sep 17 00:00:00 2001
From: Simeon Anfinrud <sanfin@chromium.org>
Date: Tue, 14 Jul 2026 14:12:30 -0700
Subject: [PATCH] [chromecast] Restrict Cast WebUI to known chrome:// hosts

CastWebService::RegisterWebUiClient registered a WebUIControllerFactory
for whatever hostnames the IPC caller supplied, and the factory matched
on hostname only, ignoring scheme. It could also be called repeatedly,
leaking a new factory into the global registry each time.

Define the set of supported Cast WebUI hosts in the browser
(IsKnownCastWebUiHost) and have CastWebUiControllerFactory filter the
requested hosts through it. GetWebUIType now also requires the chrome://
scheme. RegisterWebUiClient registers at most one factory and records
only the filtered host list, and IsCastWebUIOrigin checks the scheme as
well.

Add CastWebUiControllerFactoryTest covering the above.

Bug: 517651910
Test: cast_shell_unittests --gtest_filter=CastWebUiControllerFactoryTest.*
Change-Id: I9a90f0f04bc4bff6473f03143bab400e61d4daf0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8008577
Commit-Queue: Simeon Anfinrud <sanfin@chromium.org>
Auto-Submit: Simeon Anfinrud <sanfin@chromium.org>
Reviewed-by: Sandeep Vijayasekar <sandv@google.com>
Cr-Commit-Position: refs/heads/main@{#1662165}
---

diff --git a/chromecast/browser/BUILD.gn b/chromecast/browser/BUILD.gn
index 120ee80..0f1a647 100644
--- a/chromecast/browser/BUILD.gn
+++ b/chromecast/browser/BUILD.gn
@@ -565,6 +565,7 @@
     "devtools/cast_devtools_manager_delegate_unittest.cc",
     "lru_renderer_cache_test.cc",
     "migration/migration_utils_test.cc",
+    "webui/cast_webui_controller_factory_unittest.cc",
   ]
 
   deps = [
diff --git a/chromecast/browser/cast_web_service.cc b/chromecast/browser/cast_web_service.cc
index 5040a37..32854ff 100644
--- a/chromecast/browser/cast_web_service.cc
+++ b/chromecast/browser/cast_web_service.cc
@@ -19,12 +19,14 @@
 #include "chromecast/browser/cast_web_view_factory.h"
 #include "chromecast/browser/lru_renderer_cache.h"
 #include "chromecast/browser/webui/cast_webui_controller_factory.h"
+#include "chromecast/browser/webui/constants.h"
 #include "chromecast/chromecast_buildflags.h"
 #include "content/public/browser/browser_context.h"
 #include "content/public/browser/media_session.h"
 #include "content/public/browser/storage_partition.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_ui_controller_factory.h"
+#include "content/public/common/url_constants.h"
 #include "services/network/public/mojom/cookie_manager.mojom.h"
 
 namespace chromecast {
@@ -117,15 +119,27 @@
 }
 
 bool CastWebService::IsCastWebUIOrigin(const url::Origin& origin) {
-  return std::ranges::contains(cast_webui_hosts_, origin.host());
+  return origin.scheme() == content::kChromeUIScheme &&
+         std::ranges::contains(cast_webui_hosts_, origin.host());
 }
 
 void CastWebService::RegisterWebUiClient(
     mojo::PendingRemote<mojom::WebUiClient> client,
     const std::vector<std::string>& hosts) {
-  cast_webui_hosts_ = hosts;
+  cast_webui_hosts_.clear();
+  for (const auto& host : hosts) {
+    if (IsKnownCastWebUiHost(host)) {
+      cast_webui_hosts_.push_back(host);
+    }
+  }
+
+  if (webui_factory_registered_) {
+    return;
+  }
+
+  webui_factory_registered_ = true;
   content::WebUIControllerFactory::RegisterFactory(
-      new CastWebUiControllerFactory(std::move(client), hosts));
+      new CastWebUiControllerFactory(std::move(client), cast_webui_hosts_));
 }
 
 void CastWebService::DeleteOwnedWebViews() {
diff --git a/chromecast/browser/cast_web_service.h b/chromecast/browser/cast_web_service.h
index c6cbb1d..15e023e 100644
--- a/chromecast/browser/cast_web_service.h
+++ b/chromecast/browser/cast_web_service.h
@@ -118,6 +118,7 @@
   bool immediately_delete_webviews_ = false;
 
   std::vector<std::string> cast_webui_hosts_;
+  bool webui_factory_registered_ = false;
 
   const scoped_refptr<base::SequencedTaskRunner> task_runner_;
   base::WeakPtr<CastWebService> weak_ptr_;
diff --git a/chromecast/browser/webui/cast_webui_controller_factory.cc b/chromecast/browser/webui/cast_webui_controller_factory.cc
index fb910a48..d8fd080a 100644
--- a/chromecast/browser/webui/cast_webui_controller_factory.cc
+++ b/chromecast/browser/webui/cast_webui_controller_factory.cc
@@ -13,14 +13,28 @@
 #include "content/public/browser/url_data_source.h"
 #include "content/public/browser/web_contents.h"
 #include "content/public/browser/web_ui_controller_factory.h"
+#include "content/public/common/url_constants.h"
 #include "url/gurl.h"
 
 namespace chromecast {
+namespace {
+
+std::vector<std::string> FilterHosts(const std::vector<std::string>& hosts) {
+  std::vector<std::string> filtered;
+  for (const auto& host : hosts) {
+    if (IsKnownCastWebUiHost(host)) {
+      filtered.push_back(host);
+    }
+  }
+  return filtered;
+}
+
+}  // namespace
 
 CastWebUiControllerFactory::CastWebUiControllerFactory(
     mojo::PendingRemote<mojom::WebUiClient> client,
     const std::vector<std::string>& hosts)
-    : client_(std::move(client)), hosts_(hosts) {
+    : client_(std::move(client)), hosts_(FilterHosts(hosts)) {
   DCHECK(client_);
 }
 
@@ -29,7 +43,8 @@
 content::WebUI::TypeID CastWebUiControllerFactory::GetWebUIType(
     content::BrowserContext* browser_context,
     const GURL& url) {
-  if (std::ranges::contains(hosts_, url.GetHost())) {
+  if (url.SchemeIs(content::kChromeUIScheme) &&
+      std::ranges::contains(hosts_, url.GetHost())) {
     return const_cast<CastWebUiControllerFactory*>(this);
   }
   return content::WebUI::kNoWebUI;
diff --git a/chromecast/browser/webui/cast_webui_controller_factory_unittest.cc b/chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
new file mode 100644
index 0000000..945fa3d4
--- /dev/null
+++ b/chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
@@ -0,0 +1,48 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromecast/browser/webui/cast_webui_controller_factory.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "chromecast/browser/webui/constants.h"
+#include "base/test/task_environment.h"
+#include "content/public/browser/web_ui.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace chromecast {
+
+class CastWebUiControllerFactoryTest : public testing::Test {
+ protected:
+  CastWebUiControllerFactoryTest() {
+    auto receiver = client_.InitWithNewPipeAndPassReceiver();
+  }
+
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  mojo::PendingRemote<mojom::WebUiClient> client_;
+};
+
+TEST_F(CastWebUiControllerFactoryTest, AllowsKnownHostAndCorrectScheme) {
+  CastWebUiControllerFactory factory(std::move(client_), {kCastWebUIHomeHost});
+  EXPECT_NE(factory.GetWebUIType(nullptr, GURL("chrome://home")),
+            content::WebUI::kNoWebUI);
+}
+
+TEST_F(CastWebUiControllerFactoryTest, IgnoresUnknownHost) {
+  CastWebUiControllerFactory factory(std::move(client_), {"pwn"});
+  EXPECT_EQ(factory.GetWebUIType(nullptr, GURL("chrome://pwn")),
+            content::WebUI::kNoWebUI);
+}
+
+TEST_F(CastWebUiControllerFactoryTest, IgnoresWrongScheme) {
+  CastWebUiControllerFactory factory(std::move(client_), {kCastWebUIHomeHost});
+  EXPECT_EQ(factory.GetWebUIType(nullptr, GURL("http://home")),
+            content::WebUI::kNoWebUI);
+}
+
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chromecast/browser/webui/cast_webui_controller_factory_unittest.cc b/chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
new file mode 100644
index 0000000..945fa3d4
--- /dev/null
+++ b/chromecast/browser/webui/cast_webui_controller_factory_unittest.cc
@@ -0,0 +1,48 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "chromecast/browser/webui/cast_webui_controller_factory.h"
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "chromecast/browser/webui/constants.h"
+#include "base/test/task_environment.h"
+#include "content/public/browser/web_ui.h"
+#include "mojo/public/cpp/bindings/pending_remote.h"
+#include "testing/gtest/include/gtest/gtest.h"
+#include "url/gurl.h"
+
+namespace chromecast {
+
+class CastWebUiControllerFactoryTest : public testing::Test {
+ protected:
+  CastWebUiControllerFactoryTest() {
+    auto receiver = client_.InitWithNewPipeAndPassReceiver();
+  }
+
+  base::test::SingleThreadTaskEnvironment task_environment_;
+  mojo::PendingRemote<mojom::WebUiClient> client_;
+};
+
+TEST_F(CastWebUiControllerFactoryTest, AllowsKnownHostAndCorrectScheme) {
+  CastWebUiControllerFactory factory(std::move(client_), {kCastWebUIHomeHost});
+  EXPECT_NE(factory.GetWebUIType(nullptr, GURL("chrome://home")),
+            content::WebUI::kNoWebUI);
+}
+
+TEST_F(CastWebUiControllerFactoryTest, IgnoresUnknownHost) {
+  CastWebUiControllerFactory factory(std::move(client_), {"pwn"});
+  EXPECT_EQ(factory.GetWebUIType(nullptr, GURL("chrome://pwn")),
+            content::WebUI::kNoWebUI);
+}
+
+TEST_F(CastWebUiControllerFactoryTest, IgnoresWrongScheme) {
+  CastWebUiControllerFactory factory(std::move(client_), {kCastWebUIHomeHost});
+  EXPECT_EQ(factory.GetWebUIType(nullptr, GURL("http://home")),
+            content::WebUI::kNoWebUI);
+}
+
+}  // namespace chromecast
Loading diff…

Original Bug Report

reported by vm...@google.com

WebUI Privilege Escalation in Cast Shell via Unvalidated RegisterWebUiClient API

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: An unvalidated Mojo API, CastWebService::RegisterWebUiClient, allows a local process connected to the Cast Mojo broker to register arbitrary hosts. Because host matching in CastWebUiControllerFactory is scheme-agnostic, navigating to a chrome:// URL with the registered host bypasses standard AllowBindings checks. This potentially allows an attacker to execute custom HTML/JS with privileged WebUI bindings, resulting in a sandbox escape.

Affected files:

  • chromecast/browser/cast_web_service.cc
  • chromecast/browser/webui/cast_webui_controller_factory.cc
  • chromecast/browser/webui/cast_webui.cc
  • chromecast/browser/webui/cast_resource_data_source.cc

Estimated timestamp from git blame: 2020-07-14

Technical Analysis

1. Unvalidated Host Registration

In chromecast/browser/cast_web_service.cc (line 123), the CastWebService::RegisterWebUiClient method is implemented as follows:

void CastWebService::RegisterWebUiClient(
    mojo::PendingRemote<mojom::WebUiClient> client,
    const std::vector<std::string>& hosts) {
  cast_webui_hosts_ = hosts;
  content::WebUIControllerFactory::RegisterFactory(
      new CastWebUiControllerFactory(std::move(client), hosts));
}

This function accepts arbitrary host strings and a client-supplied remote without verifying calling permissions or restricting registrations to a validated allowlist. Additionally, each invocation registers a new CastWebUiControllerFactory via raw new, which leaks the factory into memory across the life of the process.

2. Scheme-Agnostic Host Matching

In chromecast/browser/webui/cast_webui_controller_factory.cc (line 29), GetWebUIType determines whether the factory should handle a URL purely based on the host component:

content::WebUI::TypeID CastWebUiControllerFactory::GetWebUIType(
    content::BrowserContext* browser_context,
    const GURL& url) {
  if (std::ranges::contains(hosts_, url.GetHost())) {
    return const_cast<CastWebUiControllerFactory*>(this);
  }
  return content::WebUI::kNoWebUI;
}

Because this matching check does not inspect the URL scheme, navigating to chrome://<attacker-host> successfully satisfies the check and returns a valid WebUI type.

3. Bypassing AllowBindings Validation

In content/browser/renderer_host/render_frame_host_impl.cc (line 8080), standard defense-in-depth checks verify that WebUI bindings are only granted if the target process is locked to a valid WebUI scheme:

ProcessLock process_lock = GetProcess()->GetProcessLock();
if (!process_lock.IsLockedToSite() ||
    !std::ranges::contains(URLDataManagerBackend::GetWebUISchemes(),
                           process_lock.GetProcessLockURL().GetScheme())) {
  ...
  NOTREACHED();
}

Because the navigated URL uses the "chrome" scheme (e.g., chrome://pwn), the process lock scheme evaluates to "chrome", which is present inside URLDataManagerBackend::GetWebUISchemes(). Thus, the security assertion passes, allowing high-privilege kWebUIBindingsPolicySet bindings to be granted to the renderer process.

4. Privilege Elevation & Arbitrary Code Execution

Once CastWebUI is constructed, it unconditionally enables the privileged bindings (webui->SetBindings(content::kWebUIBindingsPolicySet)) in chromecast/browser/webui/cast_webui.cc (line 28), and requests page resources from the attacker’s client remote (client->CreateController). When the renderer requests files, CastResourceDataSource::StartDataRequest retrieves raw bytes directly from the custom mojom::Resources provider controlled by the attacker (chromecast/browser/webui/cast_resource_data_source.cc, line 37). This allows arbitrary HTML/JavaScript execution in a privileged WebUI renderer, exposing privileged Mojo interfaces to the attacker and enabling a sandbox escape.

Attack Vector and Access Control

On Android, standard deployments that utilize bundle-splits stand up the ExternalMojoBroker with abstract namespace sockets (use_abstract_namespace = true) in chromecast/external_mojo/public/cpp/external_mojo_broker.cc (lines 431-432). Crucially, during connection acceptance, AcceptSocketConnection is called with check_peer_user = false (line 396), bypassing UID matching. Consequently, any unprivileged local Android application can connect to the broker, bind mojom::CastWebService, register a custom host, and escalate privileges.

Potential Steps to Reproduce (Theoretical)

(Note: These are potential steps based on source-level analysis; our tooling does not currently run executable tests.)

  1. Connect to the abstract Unix socket of the Cast Mojo Broker from a local process.
  2. Bind to the chromecast.mojom.CastWebService interface using ExternalConnector::BindInterface.
  3. Invoke RegisterWebUiClient with a custom host string (e.g., ["pwn"]) and a custom implementation of mojom::WebUiClient.
  4. Call CreateWebView to register a web view.
  5. Navigate the page to chrome://pwn.
  6. Supply custom HTML and JS utilizing MojoJS bindings via the mojom::Resources callback.
  7. Execute arbitrary Javascript with full kWebUIBindingsPolicySet bindings in the renderer, which can be leveraged to escape the sandbox.

Suggested Fix

  1. Enforce Scheme Validation: Update CastWebUiControllerFactory::GetWebUIType to explicitly require the URL to have a valid expected WebUI scheme (e.g., verifying url.SchemeIs(content::kChromeUIScheme)).
  2. Host Constraints: Restrict hosts registered via CastWebService::RegisterWebUiClient to a strict, hardcoded allowlist of permitted internal Cast WebUI host names, preventing arbitrary host injection.
  3. Broker Authorization: Secure the external Mojo broker configuration to validate peer credentials (UID) where possible, ensuring only authorized system services or split-components can access internal Cast Mojo interfaces.

Evaluated with Chrome root at commit: 5133b93d189b383c37805b1cf3a9d2dbfe8d7379


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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