Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Input
DescriptionInsufficient validation of untrusted input in Input
ComponentInput
Bug ClassLogic Error
Tracker496375695
Fix commitb885cb0c8e97 (chromium/src) +82/-23
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-19

Changed Functions

FunctionChangeNotes
if
components/input/render_widget_host_input_event_router.cc
modified
TEST_F
content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
modified

Files Changed

  • components/input/render_widget_host_input_event_router.cc
  • content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
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());
 
Loading diff…

Regression Test / PoC

shipped with the fix
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());
Loading diff…

Original Bug Report

reported by vm...@google.com

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.cc
  • components/input/render_widget_host_input_event_router.cc
  • content/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

  1. Shared Input Event Router: Fenced frames and OOPIFs are constructed such that their render_widget_delegate leads back to the embedder’s WebContentsImpl (e.g., content/browser/fenced_frame/fenced_frame.cc:61). Consequently, WebContentsImpl::GetInputEventRouter() returns a single RenderWidgetHostInputEventRouter instance that is shared across the entire frame tree.

  2. Unvalidated SetMouseCapture: A compromised renderer can send the WidgetInputHandlerHost::SetMouseCapture(true) Mojo IPC at any time. The browser’s RenderWidgetHostImpl::SetMouseCapture performs 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 calls SetMouseCaptureTarget on the shared input event router with the attacker’s own view as the target.

  3. Event Routing Hijack: When the user clicks anywhere on the embedder page, RenderWidgetHostInputEventRouter::FindMouseEventTarget determines the target. Because mouse_capture_target_ is set to the attacker’s view and IsMouseButtonDown(event) is true for MouseDown events, the router bypasses normal hit-testing and redirects the event to the attacker.

  4. 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.

  1. Load a page containing a <fencedframe> or OOPIF pointing to an attacker-controlled site.
  2. From the subframe’s compromised renderer, repeatedly invoke the WidgetInputHandlerHost::SetMouseCapture(true) Mojo IPC.
  3. The user clicks anywhere on the embedder page outside the subframe’s area.
  4. The subframe’s view receives the MouseDown event (along with subsequent drag MouseMove and MouseUp events), revealing the exact interaction coordinates relative to its origin.

Suggested Fix

There are two main areas to address:

  1. Validation: Validate the SetMouseCapture request on the browser side before calling SetMouseCaptureTarget. The browser should verify that the requesting renderer actually has an active pointer down (or is otherwise authorized to capture) before granting capture.
  2. Clamping: Ensure that coordinates sent to a capturing frame during FindMouseEventTarget and subsequent dispatch are clamped to the bounds of that frame’s RenderWidgetHostView, 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.

View on issue tracker