CVE-2026-16414
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forchromecast/browser/cast_web_service.cc |
modified | |
ifchromecast/browser/cast_web_service.cc |
modified | |
forchromecast/browser/webui/cast_webui_controller_factory.cc |
modified | |
CastWebUiControllerFactoryTestchromecast/browser/webui/cast_webui_controller_factory_unittest.cc |
modified | |
CastWebUiControllerFactoryTestchromecast/browser/webui/cast_webui_controller_factory_unittest.cc |
modified | |
TEST_Fchromecast/browser/webui/cast_webui_controller_factory_unittest.cc |
modified |
Files Changed
chromecast/browser/BUILD.gnchromecast/browser/cast_web_service.ccchromecast/browser/cast_web_service.hchromecast/browser/webui/cast_webui_controller_factory.ccchromecast/browser/webui/cast_webui_controller_factory_unittest.cc
Patch
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);
+}
+
Regression Test / PoC
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
Original Bug Report
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.ccchromecast/browser/webui/cast_webui_controller_factory.ccchromecast/browser/webui/cast_webui.ccchromecast/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.)
- Connect to the abstract Unix socket of the Cast Mojo Broker from a local process.
- Bind to the
chromecast.mojom.CastWebServiceinterface usingExternalConnector::BindInterface. - Invoke
RegisterWebUiClientwith a custom host string (e.g.,["pwn"]) and a custom implementation ofmojom::WebUiClient. - Call
CreateWebViewto register a web view. - Navigate the page to
chrome://pwn. - Supply custom HTML and JS utilizing MojoJS bindings via the
mojom::Resourcescallback. - Execute arbitrary Javascript with full
kWebUIBindingsPolicySetbindings in the renderer, which can be leveraged to escape the sandbox.
Suggested Fix
- Enforce Scheme Validation: Update
CastWebUiControllerFactory::GetWebUITypeto explicitly require the URL to have a valid expected WebUI scheme (e.g., verifyingurl.SchemeIs(content::kChromeUIScheme)). - Host Constraints: Restrict
hostsregistered viaCastWebService::RegisterWebUiClientto a strict, hardcoded allowlist of permitted internal Cast WebUI host names, preventing arbitrary host injection. - 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.