Medium watch ✔︎ verified CVSS 5.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Input
DescriptionInsufficient validation of untrusted input in Input
ComponentInput
Sub-componentInput
Bug ClassInput Validation
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
Reviewer notes: none; analysis correctly notes the renderer->SetMouseCaptureTarget IPC path and shared-router relationship are not in the shown source (hedged inferred) and that this commit adds capture authorization but not the coordinate-clamping the report also suggested.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker496375695
Fix commitb885cb0c8e97 (chromium/src) +82/-23
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-03-30 (public git)
Disclosed2026-05-19
Patch gap50 days — fix public in source before the advisory

Background

`RenderWidgetHostInputEventRouter`
the browser-process router that hit-tests and dispatches input events across a tab’s frame tree, shared by a page and its OOPIFs/fenced frames.
`mouse_capture_target_`
the view that currently receives all mouse events, bypassing normal hit-testing.
`SetMouseCaptureTarget`
the entry point (reachable from a renderer’s capture request) that sets or clears mouse_capture_target_.

Root Cause Analysis

In the pre-fix RenderWidgetHostInputEventRouter::SetMouseCaptureTarget (components/input/render_widget_host_input_event_router.cc:2137-2152), when capture is true the method unconditionally sets mouse_capture_target_ = target at :2146 after only checking touch-emulation state at :2140-2143. There is no verification that target is actually engaged in an ongoing interaction, so a compromised subframe sharing the router with its embedder can direct capture to its own view at any time and receive the embedder’s mouse events. DispatchMouseEvent (:625-711) releases capture when mouse_capture_target_ is set and the event is a kMouseUp or has no buttons down (:643-645), but that only clears an already-granted capture; it does not stop an unprivileged grant, and a frame can re-assert capture between events.

The fix adds a last_mouse_down_target_ gate: SetMouseCaptureTarget now returns early with if (target != last_mouse_down_target_) return; before :2146 (diff hunk at pre-fix :2149), so only the view that was the most recent MouseDown target may capture. DispatchMouseEvent’s capture-release block is restructured (diff at pre-fix :643-669) so that on a kMouseUp or buttonless non-kMouseDown event it also clears last_mouse_down_target_ = nullptr, preventing capture claims after the interaction ends. (Where last_mouse_down_target_ is assigned the MouseDown target is not shown in the diff — (inferred) to be set elsewhere on MouseDown, as the added unit test’s MouseDown step confirms.)

Key insight
SetMouseCaptureTarget granted capture to any requesting view with no check that it owned the active press; the patch requires the requester to be the last MouseDown target and clears that target on button release.

Attack Path

  1. Embed a hostile frame A page embeds an attacker-controlled OOPIF or fenced frame that shares its embedder’s RenderWidgetHostInputEventRouter. (inferred: the shared-router relationship is from the report, not the shown source.)
  2. Request capture unprovoked The compromised subframe repeatedly sends the capture request, and pre-fix SetMouseCaptureTarget sets mouse_capture_target_ to the subframe’s view at :2146 without any interaction check.
  3. Harvest embedder events When the user clicks or drags anywhere on the embedder, the router routes those mouse events (with their coordinates) to the capturing subframe, disclosing cross-frame interaction locations.

Impact Assessment

A compromised OOPIF or fenced frame could capture and observe the embedder’s mouse events across the whole tab, breaking Site Isolation / fenced-frame boundaries and leaking cross-origin interaction coordinates in the browser process. The advisory rates this medium severity with no in-the-wild exploitation; this commit addresses the capture authorization but not the coordinate-clamping hardening the report also suggested.

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

Audit Directions

  • IPC capability checks
    Browser-side handlers of renderer-initiated requests (capture, focus, drag) must verify the caller is authorized by current state, not accept the request unconditionally.
  • Shared cross-frame state
    Audit state shared across a frame tree (routers, capture targets) for a subframe’s ability to affect events destined for its embedder.
  • State lifetime on release
    Ensure interaction-scoped grants are cleared on the terminating event (MouseUp / no buttons) so they cannot be re-claimed out of band.
From b885cb0c8e97035a68cd77f7085b3a68b1cc212e Mon Sep 17 00:00:00 2001
From: Aman Verma <[email protected]>
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 <[email protected]>
Reviewed-by: Jonathan Ross <[email protected]>
Auto-Submit: Aman Verma <[email protected]>
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 [email protected]

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