CVE-2026-9124
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/input/render_widget_host_input_event_router.cc |
modified | |
TEST_Fcontent/browser/renderer_host/render_widget_host_input_event_router_unittest.cc |
modified |
Files Changed
components/input/render_widget_host_input_event_router.cccontent/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
Patch
From b885cb0c8e97035a68cd77f7085b3a68b1cc212e Mon Sep 17 00:00:00 2001
From: Aman Verma <amanvr@google.com>
Date: Mon, 30 Mar 2026 14:21:18 -0700
Subject: [PATCH] input: Validate SetMouseCapture requests in the browser process
Currently, a compromised or malicious renderer (such as an OOPIF or a
Fenced Frame) can hijack mouse events intended for its embedder by
sending a SetMouseCapture IPC message to the browser process. The
browser process would previously accept this request regardless of
whether a legitimate user interaction was ongoing.
This CL adds validation to RenderWidgetHostInputEventRouter to ensure
that a frame can only set mouse capture if it was the target of the
most recent MouseDown event. We also ensure that this state is cleared
when the mouse button is released (on MouseUp, or on subsequent events
like MouseMove where no buttons are held down). This prevents frames
from claiming capture after the user interaction has ended.
Bug: 496375695
Change-Id: I8b97b8c1693cd6bff7ff2adad342754f813f6f44
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7711973
Commit-Queue: Jonathan Ross <jonross@chromium.org>
Reviewed-by: Jonathan Ross <jonross@chromium.org>
Auto-Submit: Aman Verma <amanvr@google.com>
Cr-Commit-Position: refs/heads/main@{#1607373}
---
diff --git a/components/input/render_widget_host_input_event_router.cc b/components/input/render_widget_host_input_event_router.cc
index 2cb95dac..0c44667 100644
--- a/components/input/render_widget_host_input_event_router.cc
+++ b/components/input/render_widget_host_input_event_router.cc
@@ -640,32 +640,38 @@
// platforms where MouseUps are not received when the mouse cursor is off the
// browser window.
// Also, this is strictly necessary for touch emulation.
- if (mouse_capture_target_ &&
- (mouse_event.GetType() == blink::WebInputEvent::Type::kMouseUp ||
+ if (mouse_event.GetType() == blink::WebInputEvent::Type::kMouseUp ||
+ (mouse_event.GetType() != blink::WebInputEvent::Type::kMouseDown &&
!IsMouseButtonDown(mouse_event))) {
- mouse_capture_target_ = nullptr;
+ if (mouse_capture_target_) {
+ mouse_capture_target_ = nullptr;
- // Since capture is being lost it is possible that MouseMoves over a hit
- // test region might have been going to a different region, and now the
- // CursorManager might need to be notified that the view underneath the
- // cursor has changed, which could cause the display cursor to update.
- gfx::PointF transformed_point;
- auto hit_test_result =
- FindViewAtLocation(root_view, mouse_event.PositionInWidget(),
- viz::EventSource::MOUSE, &transformed_point);
- // TODO(crbug.com/41419447): This is skipped if the HitTestResult is
- // requiring an asynchronous hit test to the renderer process, because it
- // might mean sending extra MouseMoves to renderers that don't need the
- // event updates which is a worse outcome than the cursor being delayed in
- // updating. An asynchronous hit test can be added here to fix the problem.
- if (hit_test_result.view != target && !hit_test_result.should_query_view) {
- SendMouseEnterOrLeaveEvents(
- mouse_event, hit_test_result.view, root_view,
- blink::WebInputEvent::Modifiers::kRelativeMotionEvent, true);
- if (root_view->GetCursorManager())
- root_view->GetCursorManager()->UpdateViewUnderCursor(
- hit_test_result.view);
+ // Since capture is being lost it is possible that MouseMoves over a hit
+ // test region might have been going to a different region, and now the
+ // CursorManager might need to be notified that the view underneath the
+ // cursor has changed, which could cause the display cursor to update.
+ gfx::PointF transformed_point;
+ auto hit_test_result =
+ FindViewAtLocation(root_view, mouse_event.PositionInWidget(),
+ viz::EventSource::MOUSE, &transformed_point);
+ // TODO(crbug.com/41419447): This is skipped if the HitTestResult is
+ // requiring an asynchronous hit test to the renderer process, because it
+ // might mean sending extra MouseMoves to renderers that don't need the
+ // event updates which is a worse outcome than the cursor being delayed in
+ // updating. An asynchronous hit test can be added here to fix the
+ // problem.
+ if (hit_test_result.view != target &&
+ !hit_test_result.should_query_view) {
+ SendMouseEnterOrLeaveEvents(
+ mouse_event, hit_test_result.view, root_view,
+ blink::WebInputEvent::Modifiers::kRelativeMotionEvent, true);
+ if (root_view->GetCursorManager()) {
+ root_view->GetCursorManager()->UpdateViewUnderCursor(
+ hit_test_result.view);
+ }
+ }
}
+ last_mouse_down_target_ = nullptr;
}
// When touch emulation is active, mouse events have to act like touch
@@ -2143,6 +2149,12 @@
}
if (capture) {
+ // A frame should only be able to capture the mouse if it was the target of
+ // the last mouse down event. This prevents malicious frames (e.g. OOPIFs or
+ // Fenced Frames) from hijacking mouse events intended for other frames.
+ if (target != last_mouse_down_target_) {
+ return;
+ }
mouse_capture_target_ = target;
return;
}
diff --git a/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc b/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
index 4ae1071e2..a30f26fe 100644
--- a/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
@@ -204,6 +204,14 @@
return delegate_->GetInputEventRouter();
}
+ input::RenderWidgetHostViewInput* mouse_capture_target() {
+ return rwhier()->mouse_capture_target_;
+ }
+
+ input::RenderWidgetHostViewInput* last_mouse_down_target() {
+ return rwhier()->last_mouse_down_target_;
+ }
+
// testing::Test:
void SetUp() override {
browser_context_ = std::make_unique<TestBrowserContext>();
@@ -1232,6 +1240,45 @@
EXPECT_FALSE(targeter->is_auto_scroll_in_progress());
}
+// Tests that SetMouseCaptureTarget only allows a frame to capture the mouse if
+// it was the target of the most recent MouseDown event.
+TEST_F(RenderWidgetHostInputEventRouterTest, SetMouseCaptureTargetValidation) {
+ ChildViewState child = MakeChildView(view_root_.get());
+
+ // 1. Request capture without a prior MouseDown. Should be rejected.
+ rwhier()->SetMouseCaptureTarget(child.view.get(), true);
+ EXPECT_EQ(nullptr, mouse_capture_target());
+
+ // 2. Simulate a MouseDown on the child view.
+ blink::WebMouseEvent mouse_down_event(
+ blink::WebInputEvent::Type::kMouseDown,
+ blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ mouse_down_event.button = blink::WebPointerProperties::Button::kLeft;
+
+ view_root_->SetHittestResult(child.view.get(), false);
+ rwhier()->RouteMouseEvent(view_root_.get(), &mouse_down_event,
+ ui::LatencyInfo());
+
+ // Verify the child is now the last mouse down target.
+ EXPECT_EQ(child.view.get(), last_mouse_down_target());
+
+ // 3. Request capture again. Should be accepted.
+ rwhier()->SetMouseCaptureTarget(child.view.get(), true);
+ EXPECT_EQ(child.view.get(), mouse_capture_target());
+
+ // 4. Simulate a MouseUp. The capture and last mouse down target should be
+ // cleared.
+ blink::WebMouseEvent mouse_up_event(
+ blink::WebInputEvent::Type::kMouseUp, blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ mouse_up_event.button = blink::WebPointerProperties::Button::kLeft;
+ rwhier()->RouteMouseEvent(view_root_.get(), &mouse_up_event,
+ ui::LatencyInfo());
+ EXPECT_EQ(nullptr, last_mouse_down_target());
+ EXPECT_EQ(nullptr, mouse_capture_target());
+}
+
TEST_F(RenderWidgetHostInputEventRouterTest, QueryResultAfterChildViewDead) {
ChildViewState child = MakeChildView(view_root_.get());
Regression Test / PoC
diff --git a/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc b/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
index 4ae1071e2..a30f26fe 100644
--- a/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
@@ -204,6 +204,14 @@
return delegate_->GetInputEventRouter();
}
+ input::RenderWidgetHostViewInput* mouse_capture_target() {
+ return rwhier()->mouse_capture_target_;
+ }
+
+ input::RenderWidgetHostViewInput* last_mouse_down_target() {
+ return rwhier()->last_mouse_down_target_;
+ }
+
// testing::Test:
void SetUp() override {
browser_context_ = std::make_unique<TestBrowserContext>();
@@ -1232,6 +1240,45 @@
EXPECT_FALSE(targeter->is_auto_scroll_in_progress());
}
+// Tests that SetMouseCaptureTarget only allows a frame to capture the mouse if
+// it was the target of the most recent MouseDown event.
+TEST_F(RenderWidgetHostInputEventRouterTest, SetMouseCaptureTargetValidation) {
+ ChildViewState child = MakeChildView(view_root_.get());
+
+ // 1. Request capture without a prior MouseDown. Should be rejected.
+ rwhier()->SetMouseCaptureTarget(child.view.get(), true);
+ EXPECT_EQ(nullptr, mouse_capture_target());
+
+ // 2. Simulate a MouseDown on the child view.
+ blink::WebMouseEvent mouse_down_event(
+ blink::WebInputEvent::Type::kMouseDown,
+ blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ mouse_down_event.button = blink::WebPointerProperties::Button::kLeft;
+
+ view_root_->SetHittestResult(child.view.get(), false);
+ rwhier()->RouteMouseEvent(view_root_.get(), &mouse_down_event,
+ ui::LatencyInfo());
+
+ // Verify the child is now the last mouse down target.
+ EXPECT_EQ(child.view.get(), last_mouse_down_target());
+
+ // 3. Request capture again. Should be accepted.
+ rwhier()->SetMouseCaptureTarget(child.view.get(), true);
+ EXPECT_EQ(child.view.get(), mouse_capture_target());
+
+ // 4. Simulate a MouseUp. The capture and last mouse down target should be
+ // cleared.
+ blink::WebMouseEvent mouse_up_event(
+ blink::WebInputEvent::Type::kMouseUp, blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ mouse_up_event.button = blink::WebPointerProperties::Button::kLeft;
+ rwhier()->RouteMouseEvent(view_root_.get(), &mouse_up_event,
+ ui::LatencyInfo());
+ EXPECT_EQ(nullptr, last_mouse_down_target());
+ EXPECT_EQ(nullptr, mouse_capture_target());
+}
+
TEST_F(RenderWidgetHostInputEventRouterTest, QueryResultAfterChildViewDead) {
ChildViewState child = MakeChildView(view_root_.get());
Original Bug Report
Site Isolation bypass: OOPIF or Fenced Frame can hijack embedder mouse events via SetMouseCapture
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer (e.g., a Fenced Frame or OOPIF) can potentially use the unvalidated WidgetInputHandlerHost::SetMouseCapture(true) Mojo IPC to capture all mouse events globally across its embedder’s tab. Because these frames share a single RenderWidgetHostInputEventRouter with their embedder and coordinate transforms are not clamped, the attacker can observe user interactions outside its own bounds.
Affected files:
content/browser/renderer_host/render_widget_host_impl.cccomponents/input/render_widget_host_input_event_router.cccontent/browser/fenced_frame/fenced_frame.cc
Estimated timestamp from git blame: 2024-05-28
Summary
A potential vulnerability exists where a compromised renderer (such as an out-of-process iframe or a fenced frame) can hijack all mouse events (clicks, drags, moves) from its embedder. This is due to a lack of validation in the RenderWidgetHostImpl::SetMouseCapture IPC handler combined with the fact that these subframes share a single RenderWidgetHostInputEventRouter with their parent WebContentsImpl.
Technical Details
-
Shared Input Event Router: Fenced frames and OOPIFs are constructed such that their
render_widget_delegateleads back to the embedder’sWebContentsImpl(e.g.,content/browser/fenced_frame/fenced_frame.cc:61). Consequently,WebContentsImpl::GetInputEventRouter()returns a singleRenderWidgetHostInputEventRouterinstance that is shared across the entire frame tree. -
Unvalidated SetMouseCapture: A compromised renderer can send the
WidgetInputHandlerHost::SetMouseCapture(true)Mojo IPC at any time. The browser’sRenderWidgetHostImpl::SetMouseCaptureperforms no validation to ensure that the calling renderer is authorized to capture mouse events (e.g., verifying it currently has an active pointer down). It unconditionally callsSetMouseCaptureTargeton the shared input event router with the attacker’s own view as the target. -
Event Routing Hijack: When the user clicks anywhere on the embedder page,
RenderWidgetHostInputEventRouter::FindMouseEventTargetdetermines the target. Becausemouse_capture_target_is set to the attacker’s view andIsMouseButtonDown(event)is true forMouseDownevents, the router bypasses normal hit-testing and redirects the event to the attacker. -
Unclamped Coordinates: The event’s coordinates are transformed into the attacker’s local coordinate space using
TransformPointToCoordSpaceForView. Crucially, this transformation performs matrix math but does not clamp the resulting coordinates to the bounds of the capturing view. As a result, the attacker receives negative coordinates or coordinates exceeding their frame size, revealing the exact location of the user’s interaction globally across the embedder page.
While DispatchMouseEvent clears capture on buttonless MouseMove events, an attacker can maintain capture by repeatedly calling the SetMouseCapture IPC, exploiting the typical timing gap between mouse movement and a user click.
Impact
This behavior violates the isolation guarantees of fenced frames and Site Isolation for out-of-process iframes. A compromised renderer can observe user interaction coordinates and drag paths outside its own boundaries, enabling cross-site information disclosure and facilitating UI redressing or clickjacking attacks.
Suggested Exploit Steps (Theoretical)
Note: These are theoretical steps, as we do not yet have the ability to run code to confirm them.
- Load a page containing a
<fencedframe>or OOPIF pointing to an attacker-controlled site. - From the subframe’s compromised renderer, repeatedly invoke the
WidgetInputHandlerHost::SetMouseCapture(true)Mojo IPC. - The user clicks anywhere on the embedder page outside the subframe’s area.
- The subframe’s view receives the
MouseDownevent (along with subsequent dragMouseMoveandMouseUpevents), revealing the exact interaction coordinates relative to its origin.
Suggested Fix
There are two main areas to address:
- Validation: Validate the
SetMouseCapturerequest on the browser side before callingSetMouseCaptureTarget. The browser should verify that the requesting renderer actually has an active pointer down (or is otherwise authorized to capture) before granting capture. - Clamping: Ensure that coordinates sent to a capturing frame during
FindMouseEventTargetand subsequent dispatch are clamped to the bounds of that frame’sRenderWidgetHostView, preventing the leakage of global coordinate data.
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.