CVE-2026-10912
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
EventRouterExploitTestchrome/browser/extensions/extension_security_exploit_browsertest.cc |
modified |
Files Changed
chrome/browser/extensions/extension_security_exploit_browsertest.cc
Patch
From 5cca17e7f0c540b6c8b6472e1c04e0aa5a65440d Mon Sep 17 00:00:00 2001
From: Tim Judkins <tjudkins@chromium.org>
Date: Fri, 17 Apr 2026 12:46:44 -0700
Subject: [PATCH] [Extensions] Add extra validation and bad messages to EventRouter
This CL adds some hardening to registering listeners with the extensions
EventRouter. Specifically:
* When an extension_id is specified we check that the render process is
either an extension process for that ID or a process which that
extension ID has injected a content or user script into.
* When a listener_url is specified we verify that the process can
access the origin for the URL.
* When a scope_url is specified (when registering for a service worker)
we verify the process can access the origin of the scope URL.
If any of these checks fail we emit a bad message and kill the renderer.
Also adds related tests to cover all these cases and verify the bad
message emitted.
Bug: 496614553
Change-Id: I289056d78432fc8e96e1b47b24b0711641bc3d65
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7745162
Reviewed-by: Andrea Orru <andreaorru@chromium.org>
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Commit-Queue: Tim <tjudkins@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1616788}
---
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 85d5859..d6de6e61 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -39,10 +39,13 @@
#include "extensions/browser/service_worker/service_worker_host.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_features.h"
+#include "extensions/common/mojom/event_router.mojom.h"
#include "extensions/common/mojom/frame.mojom-test-utils.h"
#include "extensions/common/mojom/service_worker_host.mojom-test-utils.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
@@ -958,4 +961,369 @@
*main_frame->GetProcess(), extension_b->id()));
}
+class EventRouterExploitTest : public ExtensionSecurityExploitBrowserTest {
+ public:
+ EventRouterExploitTest() = default;
+
+ void SetUpOnMainThread() override {
+ ExtensionSecurityExploitBrowserTest::SetUpOnMainThread();
+ // This installs two extensions (`active_extension()` and
+ // `spoofed_extension()`) which are identical except for differing extension
+ // IDs. In tests below we treat them so `active_extension()` should have
+ // access to do things, while `spoofed_extension()` should not.
+ InstallTestExtensions();
+ }
+};
+
+// Tests that attempting to add a main thread listener for the process of a
+// webpage by specifying a listener URL will work for a URL the process has
+// access to (i.e. the actual URL of the page), but fail for one it does not
+// have access.
+IN_PROC_BROWSER_TEST_F(EventRouterExploitTest,
+ AddListenerForMainThread_BadListenerUrl) {
+ GURL test_page_url =
+ embedded_test_server()->GetURL("foo.com", "/title1.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ mojo::AssociatedRemote<mojom::EventRouter> event_router;
+ EventRouter::BindForRenderer(
+ main_frame_process->GetID(),
+ event_router.BindNewEndpointAndPassDedicatedReceiver());
+ EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+
+ {
+ // Try to add a listener for the actual URL of the page. This should be
+ // fine.
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewListenerUrl(test_page_url);
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+ }
+
+ {
+ // Try to add a listener for "chrome://settings". It should trigger a bad
+ // message as the process doesn't have access to that origin.
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewListenerUrl(GURL("chrome://settings"));
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_EQ(
+ "Tried to add an event listener for an unauthorized listener URL.",
+ bad_message_observer.WaitForBadMessage());
+ }
+}
+
+// Tests that attempting to add a main thread listener for the process of a
+// webpage by specifying an extension ID will work for an extension that has
+// injected a content script, but fail for one that has not.
+IN_PROC_BROWSER_TEST_F(
+ EventRouterExploitTest,
+ AddListenerForMainThread_SpoofedExtensionIdWithContentScript) {
+ GURL test_page_url =
+ embedded_test_server()->GetURL("foo.com", "/title1.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ // Simulate a content script from `active_extension()` being injected into the
+ // process.
+ ScriptInjectionTracker::AddExtensionThatRanContentScriptsInProcessForTesting(
+ *main_frame_process, active_extension_id());
+
+ mojo::AssociatedRemote<mojom::EventRouter> event_router;
+ EventRouter::BindForRenderer(
+ main_frame_process->GetID(),
+ event_router.BindNewEndpointAndPassDedicatedReceiver());
+
+ EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+
+ {
+ // Try to add a listener for `active_extension_id()`. This should be fine
+ // because the extension will appear to have run a content script in the
+ // page.
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(active_extension_id());
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+ }
+
+ {
+ // Try to add a listener for `spoofed_extension_id()`. It should trigger a
+ // bad message because the content script was injected for
+ // `active_extension_id()`.
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_EQ(
+ "Tried to add an event listener for an unauthorized extension ID.",
+ bad_message_observer.WaitForBadMessage());
+ }
+}
+
+// Tests that attempting to add a main thread listener for the process of a
+// webpage by specifying an extension ID will work for an extension that has
+// injected a user script, but fail for one that has not.
+IN_PROC_BROWSER_TEST_F(
+ EventRouterExploitTest,
+ AddListenerForMainThread_SpoofedExtensionIdWithUserScript) {
+ GURL test_page_url =
+ embedded_test_server()->GetURL("foo.com", "/title1.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ // Simulate a user script from `active_extension()` being injected into the
+ // process.
+ ScriptInjectionTracker::AddExtensionThatRanUserScriptsInProcessForTesting(
+ *main_frame_process, active_extension_id());
Regression Test / PoC
diff --git a/chrome/browser/extensions/extension_security_exploit_browsertest.cc b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
index 85d5859..d6de6e61 100644
--- a/chrome/browser/extensions/extension_security_exploit_browsertest.cc
+++ b/chrome/browser/extensions/extension_security_exploit_browsertest.cc
@@ -39,10 +39,13 @@
#include "extensions/browser/service_worker/service_worker_host.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension_features.h"
+#include "extensions/common/mojom/event_router.mojom.h"
#include "extensions/common/mojom/frame.mojom-test-utils.h"
#include "extensions/common/mojom/service_worker_host.mojom-test-utils.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/test_extension_dir.h"
+#include "mojo/public/cpp/bindings/associated_remote.h"
+#include "mojo/public/cpp/test_support/test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
@@ -958,4 +961,369 @@
*main_frame->GetProcess(), extension_b->id()));
}
+class EventRouterExploitTest : public ExtensionSecurityExploitBrowserTest {
+ public:
+ EventRouterExploitTest() = default;
+
+ void SetUpOnMainThread() override {
+ ExtensionSecurityExploitBrowserTest::SetUpOnMainThread();
+ // This installs two extensions (`active_extension()` and
+ // `spoofed_extension()`) which are identical except for differing extension
+ // IDs. In tests below we treat them so `active_extension()` should have
+ // access to do things, while `spoofed_extension()` should not.
+ InstallTestExtensions();
+ }
+};
+
+// Tests that attempting to add a main thread listener for the process of a
+// webpage by specifying a listener URL will work for a URL the process has
+// access to (i.e. the actual URL of the page), but fail for one it does not
+// have access.
+IN_PROC_BROWSER_TEST_F(EventRouterExploitTest,
+ AddListenerForMainThread_BadListenerUrl) {
+ GURL test_page_url =
+ embedded_test_server()->GetURL("foo.com", "/title1.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ mojo::AssociatedRemote<mojom::EventRouter> event_router;
+ EventRouter::BindForRenderer(
+ main_frame_process->GetID(),
+ event_router.BindNewEndpointAndPassDedicatedReceiver());
+ EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+
+ {
+ // Try to add a listener for the actual URL of the page. This should be
+ // fine.
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewListenerUrl(test_page_url);
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+ }
+
+ {
+ // Try to add a listener for "chrome://settings". It should trigger a bad
+ // message as the process doesn't have access to that origin.
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewListenerUrl(GURL("chrome://settings"));
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_EQ(
+ "Tried to add an event listener for an unauthorized listener URL.",
+ bad_message_observer.WaitForBadMessage());
+ }
+}
+
+// Tests that attempting to add a main thread listener for the process of a
+// webpage by specifying an extension ID will work for an extension that has
+// injected a content script, but fail for one that has not.
+IN_PROC_BROWSER_TEST_F(
+ EventRouterExploitTest,
+ AddListenerForMainThread_SpoofedExtensionIdWithContentScript) {
+ GURL test_page_url =
+ embedded_test_server()->GetURL("foo.com", "/title1.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ // Simulate a content script from `active_extension()` being injected into the
+ // process.
+ ScriptInjectionTracker::AddExtensionThatRanContentScriptsInProcessForTesting(
+ *main_frame_process, active_extension_id());
+
+ mojo::AssociatedRemote<mojom::EventRouter> event_router;
+ EventRouter::BindForRenderer(
+ main_frame_process->GetID(),
+ event_router.BindNewEndpointAndPassDedicatedReceiver());
+
+ EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+
+ {
+ // Try to add a listener for `active_extension_id()`. This should be fine
+ // because the extension will appear to have run a content script in the
+ // page.
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(active_extension_id());
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+ }
+
+ {
+ // Try to add a listener for `spoofed_extension_id()`. It should trigger a
+ // bad message because the content script was injected for
+ // `active_extension_id()`.
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_EQ(
+ "Tried to add an event listener for an unauthorized extension ID.",
+ bad_message_observer.WaitForBadMessage());
+ }
+}
+
+// Tests that attempting to add a main thread listener for the process of a
+// webpage by specifying an extension ID will work for an extension that has
+// injected a user script, but fail for one that has not.
+IN_PROC_BROWSER_TEST_F(
+ EventRouterExploitTest,
+ AddListenerForMainThread_SpoofedExtensionIdWithUserScript) {
+ GURL test_page_url =
+ embedded_test_server()->GetURL("foo.com", "/title1.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ // Simulate a user script from `active_extension()` being injected into the
+ // process.
+ ScriptInjectionTracker::AddExtensionThatRanUserScriptsInProcessForTesting(
+ *main_frame_process, active_extension_id());
+
+ mojo::AssociatedRemote<mojom::EventRouter> event_router;
+ EventRouter::BindForRenderer(
+ main_frame_process->GetID(),
+ event_router.BindNewEndpointAndPassDedicatedReceiver());
+
+ EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+
+ {
+ // Try to add a listener for `active_extension_id()`. This should be fine
+ // because the extension will appear to have run a user script on the page.
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(active_extension_id());
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+ }
+
+ {
+ // Try to add a listener for `spoofed_extension_id()`. It should trigger a
+ // bad message because the user script was injected for
+ // `active_extension_id()`.
+ mojo::test::BadMessageObserver bad_message_observer;
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
+ event_listener->event_name = "test.eventName";
+
+ event_router->AddListenerForMainThread(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_EQ(
+ "Tried to add an event listener for an unauthorized extension ID.",
+ bad_message_observer.WaitForBadMessage());
+ }
+}
+
+// Tests that attempting to add a service worker listener to the process of a
+// webpage and specifying an extension ID as the `listener_owners` will fail.
+IN_PROC_BROWSER_TEST_F(EventRouterExploitTest,
+ AddListenerForServiceWorker_BadExtensionIdForWebPage) {
+ GURL test_page_url =
+ embedded_test_server()->GetURL("foo.com", "/title1.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ mojo::AssociatedRemote<mojom::EventRouter> event_router;
+ EventRouter::BindForRenderer(
+ main_frame_process->GetID(),
+ event_router.BindNewEndpointAndPassDedicatedReceiver());
+
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(spoofed_extension_id());
+ event_listener->event_name = "test.eventName";
+ event_listener->service_worker_context = mojom::ServiceWorkerContext::New();
+ event_listener->service_worker_context->scope_url = spoofed_extension().url();
+ event_listener->service_worker_context->version_id = 1;
+ event_listener->service_worker_context->thread_id = 1;
+
+ event_router->AddListenerForServiceWorker(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_EQ("Tried to add an event listener for an unauthorized extension ID.",
+ bad_message_observer.WaitForBadMessage());
+}
+
+// Tests that attempting to add a service worker listener to the process of an
+// extension page works for that extension, but specifying an extension ID of
+// another extension will fail.
+IN_PROC_BROWSER_TEST_F(
+ EventRouterExploitTest,
+ AddListenerForServiceWorker_BadExtensionIdForExtensionPage) {
+ // Navigate to an extension page, so the associated process is authorized for
+ // `active_extension_id()`.
+ GURL test_page_url = active_extension().GetResourceURL("page.html");
+ auto* web_contents = GetActiveWebContents();
+ EXPECT_TRUE(NavigateToURL(web_contents, test_page_url));
+
+ content::RenderProcessHost* main_frame_process =
+ web_contents->GetPrimaryMainFrame()->GetProcess();
+
+ mojo::AssociatedRemote<mojom::EventRouter> event_router;
+ EventRouter::BindForRenderer(
+ main_frame_process->GetID(),
+ event_router.BindNewEndpointAndPassDedicatedReceiver());
+
+ EXPECT_FALSE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+
+ {
+ // Try adding a listener for the extension that owns the page. This should
+ // be fine.
+ auto event_listener = mojom::EventListener::New();
+ event_listener->listener_owner =
+ mojom::EventListenerOwner::NewExtensionId(active_extension_id());
+ event_listener->event_name = "test.eventName";
+ event_listener->service_worker_context = mojom::ServiceWorkerContext::New();
+ event_listener->service_worker_context->scope_url =
+ active_extension().url();
+ event_listener->service_worker_context->version_id = 1;
+ event_listener->service_worker_context->thread_id = 1;
+
+ event_router->AddListenerForServiceWorker(std::move(event_listener));
+ event_router.FlushForTesting();
+
+ EXPECT_TRUE(EventRouter::Get(profile())->HasNonLazyEventListenerForTesting(
+ "test.eventName"));
+ }
+
+ {
+ // Try adding a listener but specifying a `listener_owner` for a different
+ // extension ID. This should trigger a bad message.
+ mojo::test::BadMessageObserver bad_message_observer;
+
+ auto event_listener = mojom::EventListener::New();
+ // Pass the `listener_owner` as the ID of the `spoofed_extension()`.
... (truncated)
Original Bug Report
Renderer Sandbox Bypass via Spoofed EventRouter Listener URL
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can spoof its origin when registering for Extension EventRouter events via Mojo. This allows an attacker to bypass origin and context type restrictions, intercepting sensitive API events (e.g., fileSystemProvider, sockets.tcp) intended for privileged contexts like chrome-untrusted://terminal/.
Affected files:
extensions/browser/events/event_dispatch_helper.ccextensions/browser/event_router.ccextensions/browser/process_map.ccchrome/browser/ash/file_system_provider/request_dispatcher_impl.cc
Estimated timestamp from git blame: 2024-08-01
Summary
A vulnerability in the Extensions EventRouter allows a compromised renderer process to spoof its origin during event listener registration. By providing an arbitrary listener_url to the mojom::EventRouter::AddListenerForMainThread Mojo interface, the renderer can bypass origin-based event restrictions and context type validation. This allows the attacker to intercept sensitive events, such as file system operations (fileSystemProvider) and raw socket data (sockets.tcp), intended for specific WebUI contexts like the ChromeOS Terminal app.
Note: These are potential steps and findings as our setup does not currently have the ability to run code or provide a working proof of concept.
Technical Details
-
Lack of Origin Validation in Event Registration: The
mojom::EventRouterinterface is exposed to all render frames (viaChromeContentBrowserClientExtensionsPart::ExposeInterfacesToRendererForRenderFrameHost). Inextensions/browser/event_router.cc, theAddListenerForMainThreadmethod accepts thelistener_urlprovided by the renderer without validating that the renderer’sChildProcessSecurityPolicyis authorized for that origin. It only verifies that the URL is syntactically valid. -
Origin Restriction Bypass: When sensitive events are dispatched using
DispatchEventToURL(restrict_to_url, event)(e.g., fromRequestDispatcherImpl::DispatchRequestfor the Terminal app), the system checks if the listener’s origin matches the restriction. Inextensions/browser/events/event_dispatch_helper.cc,ListenerMeetsRestrictionscompares therestrict_to_urlagainst the spoofedlistener->listener_url(). Because both origins match (chrome-untrusted://terminal/), this check incorrectly passes. -
Context Type Misclassification: During dispatch,
EventDispatchHelpercallsProcessMap::GetMostLikelyContextType(extensions/browser/process_map.cc) to determine the context type. Because the listener was added via URL (meaning theextensionpointer is null),GetMostLikelyContextTypebases its decision entirely on the scheme of the spoofed URL. Seeing thechrome-untrusted://scheme, it returnsmojom::ContextType::kUntrustedWebUi, bypassing any validation against the actual process’s site instance. -
Feature Availability Bypass: With the origin matching and the context misclassified as
kUntrustedWebUi,EventDispatchHelper::CheckFeatureAvailabilityconsults_api_features.json. Since APIs likefileSystemProviderandsockets.tcpare allowed forkUntrustedWebUicontexts matchingchrome-untrusted://terminal/*, the check succeeds. The sensitive event data is then dispatched via IPC to the attacker’s compromised regular web renderer process.
Potential Attacker Steps to Trigger the Vulnerability
- Gain arbitrary code execution within a regular web renderer process (e.g., browsing
https://evil.com). - Use Mojo to bind the
mojom::EventRouterinterface, which is exposed to all render frames. - Send an
AddListenerForMainThreadIPC message. Set theevent_nameto a sensitive API event (e.g.,fileSystemProvider.onReadFileRequestedorsockets.tcp.onReceive) and spoof thelistener_ownerURL tochrome-untrusted://terminal/. - Wait for a legitimate system action to trigger the event (e.g., the user interacts with an SFTP mount in the ChromeOS Terminal).
- The browser process will dispatch the event containing sensitive cross-origin data directly to the compromised renderer process, bypassing the sandbox.
Suggested Fix
Add origin validation in EventRouter::AddListenerForMainThread and EventRouter::AddListenerForServiceWorker when processing a listener_url. Specifically, before calling AddEventListenerForURL, verify that the RenderProcessHost is authorized to commit or represent the provided listener_url (e.g., using ChildProcessSecurityPolicy::GetInstance()->CanCommitURL(process_id, listener_url)). Additionally, ProcessMap::GetMostLikelyContextType could be hardened to verify the actual process type when extension is null, rather than relying solely on the URL scheme.
Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8
Results 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. Please feel free to reach out to me if you have concerns or feedback.