Overview

High
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
Tracker502681591
Fix commit5b1580197b3c (chromium/src) +312/-42
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
while
components/input/render_widget_host_input_event_router.cc
modified
if
components/input/render_widget_host_input_event_router.cc
modified
while
components/input/render_widget_host_view_input.cc
modified
if
components/input/render_widget_host_view_input.cc
modified

Files Changed

  • components/input/render_widget_host_input_event_router.cc
  • components/input/render_widget_host_input_event_router.h
  • components/input/render_widget_host_view_input.cc
  • components/input/render_widget_host_view_input.h
From 5b1580197b3cbe292f4b3a5cb3a68e19ed485fbf Mon Sep 17 00:00:00 2001
From: Jonathan Ross <[email protected]>
Date: Thu, 21 May 2026 13:13:39 -0700
Subject: [PATCH] input: Harden RenderWidgetTargeter autoscroll

This CL hardens RenderWidgetTargeter validation of autoscroll state.

1. Coordinate Transformation: When using a cached middle-click target
during autoscroll,
   ensure the event coordinates are transformed from the root view's space to the
   target view's space.
2. State Validation: Ensure that autoscroll can only be started by the
view that
   actually received the middle click, preventing other frames from hijacking the
   autoscroll state.
3. Support Asynchronous input targeting, and the race with the Fling
Start from Renderers. By caching each pending aspect, and resolving when
we have a target

Bug: 502681591
Change-Id: Ib88d1bc0eed6a4f8dc8c4b301665e8456e4297b1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7792784
Commit-Queue: Jonathan Ross <[email protected]>
Reviewed-by: Charlie Reis <[email protected]>
Reviewed-by: Kartar Singh <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1634488}
---

diff --git a/components/input/render_widget_host_input_event_router.cc b/components/input/render_widget_host_input_event_router.cc
index 1c8ad393..14470d5 100644
--- a/components/input/render_widget_host_input_event_router.cc
+++ b/components/input/render_widget_host_input_event_router.cc
@@ -1153,25 +1153,6 @@
 
 namespace {
 
-// Returns true if |target_view| is one of |starting_view|'s ancestors.
-// If |stay_within| is provided, we only consider ancestors within that
-// sub-tree.
-bool IsAncestorView(RenderWidgetHostViewInput* starting_view,
-                    const RenderWidgetHostViewInput* target_view,
-                    const RenderWidgetHostViewInput* stay_within = nullptr) {
-  RenderWidgetHostViewInput* cur_view = starting_view->GetParentViewInput();
-  while (cur_view) {
-    if (cur_view == target_view)
-      return true;
-
-    if (stay_within && cur_view == stay_within)
-      return false;
-
-    cur_view = cur_view->GetParentViewInput();
-  }
-  return false;
-}
-
 // Given |event| in root coordinates, return an event in |target_view|'s
 // coordinates.
 blink::WebGestureEvent GestureEventInTarget(
@@ -1273,8 +1254,8 @@
 
     bubbling_gesture_scroll_target_ = target_view;
     bubbling_gesture_scroll_source_device_ = event.SourceDevice();
-    DCHECK(IsAncestorView(bubbling_gesture_scroll_origin_,
-                          bubbling_gesture_scroll_target_));
+    DCHECK(RenderWidgetHostViewInput::IsAncestorView(
+        bubbling_gesture_scroll_origin_, bubbling_gesture_scroll_target_));
   } else {  // !(event.GetType() ==
             // blink::WebInputEvent::Type::kGestureScrollBegin)
     if (!bubbling_gesture_scroll_target_) {
@@ -1410,7 +1391,8 @@
   // We cancel bubbling only when the child view affects the current scroll
   // bubbling sequence.
   if (detaching_view == bubbling_gesture_scroll_origin_ ||
-      IsAncestorView(bubbling_gesture_scroll_origin_, detaching_view)) {
+      RenderWidgetHostViewInput::IsAncestorView(bubbling_gesture_scroll_origin_,
+                                                detaching_view)) {
     CancelScrollBubbling();
   }
 }
@@ -1448,8 +1430,9 @@
   if (!bubbling_gesture_scroll_target_ || !bubbling_gesture_scroll_origin_)
     return;
 
-  if (IsAncestorView(bubbling_gesture_scroll_origin_, target,
-                     bubbling_gesture_scroll_target_)) {
+  if (RenderWidgetHostViewInput::IsAncestorView(
+          bubbling_gesture_scroll_origin_, target,
+          bubbling_gesture_scroll_target_)) {
     CancelScrollBubbling();
   }
 }
@@ -1910,7 +1893,7 @@
       iter == owner_map_.end() ? nullptr : iter->second.get();
 
   if (view && ancestor_to_verify && view != ancestor_to_verify &&
-      !IsAncestorView(view, ancestor_to_verify)) {
+      !RenderWidgetHostViewInput::IsAncestorView(view, ancestor_to_verify)) {
     return nullptr;
   }
 
@@ -2175,9 +2158,17 @@
   root_view_receive_additional_mouse_up_ = root_view_receives_mouse_up;
 }
 
-void RenderWidgetHostInputEventRouter::SetAutoScrollInProgress(
+RenderWidgetTargeter::AutoscrollStatus
+RenderWidgetHostInputEventRouter::SetAutoScrollInProgress(
+    RenderWidgetHostViewInput* view,
     bool is_autoscroll_in_progress) {
-  event_targeter_->SetIsAutoScrollInProgress(is_autoscroll_in_progress);
+  return event_targeter_->SetIsAutoScrollInProgress(view,
+                                                    is_autoscroll_in_progress);
+}
+
+void RenderWidgetHostInputEventRouter::CancelAutoscroll(
+    RenderWidgetHostViewInput* view) {
+  delegate_->CancelAutoscroll(view);
 }
 
 bool IsMoveEvent(ui::EventType type) {
diff --git a/components/input/render_widget_host_input_event_router.h b/components/input/render_widget_host_input_event_router.h
index 143badd0..0515faa5 100644
--- a/components/input/render_widget_host_input_event_router.h
+++ b/components/input/render_widget_host_input_event_router.h
@@ -115,6 +115,7 @@
    public:
     virtual ~Delegate() = default;
     virtual TouchEmulator* GetTouchEmulator(bool create_if_necessary) = 0;
+    virtual void CancelAutoscroll(RenderWidgetHostViewInput* view) = 0;
   };
 
   explicit RenderWidgetHostInputEventRouter(viz::HitTestDataProvider* provider,
@@ -193,6 +194,7 @@
       RenderWidgetHostViewInput* ancestor_to_verify = nullptr) const override;
   bool ShouldContinueHitTesting(
       RenderWidgetHostViewInput* target_view) const override;
+  void CancelAutoscroll(RenderWidgetHostViewInput* view) override;
 
   // Allows a target to claim or release capture of mouse events.
   void SetMouseCaptureTarget(RenderWidgetHostViewInput* target,
@@ -235,12 +237,18 @@
 
   size_t TouchEventAckQueueLengthForTesting() const;
   size_t RegisteredViewCountForTesting() const;
+  const gfx::PointF& mouse_down_post_transformed_coordinate_for_testing()
+      const {
+    return mouse_down_post_transformed_coordinate_;
+  }
 
   void set_route_to_root_for_devtools(bool route) {
     route_to_root_for_devtools_ = route;
   }
 
-  void SetAutoScrollInProgress(bool is_autoscroll_in_progress);
+  RenderWidgetTargeter::AutoscrollStatus SetAutoScrollInProgress(
+      RenderWidgetHostViewInput* view,
+      bool is_autoscroll_in_progress);
 
   RenderWidgetHostViewInput* GetLastMouseMoveTargetForTest();
   RenderWidgetHostViewInput* GetLastMouseMoveRootViewForTest();
diff --git a/components/input/render_widget_host_view_input.cc b/components/input/render_widget_host_view_input.cc
index a70fc35..4fd0b80 100644
--- a/components/input/render_widget_host_view_input.cc
+++ b/components/input/render_widget_host_view_input.cc
@@ -150,6 +150,26 @@
   return nullptr;
 }
 
+// static
+bool RenderWidgetHostViewInput::IsAncestorView(
+    RenderWidgetHostViewInput* starting_view,
+    const RenderWidgetHostViewInput* target_view,
+    const RenderWidgetHostViewInput* stay_within) {
+  RenderWidgetHostViewInput* cur_view = starting_view->GetParentViewInput();
+  while (cur_view) {
+    if (cur_view == target_view) {
+      return true;
+    }
+
+    if (stay_within && cur_view == stay_within) {
+      return false;
+    }
+
+    cur_view = cur_view->GetParentViewInput();
+  }
+  return false;
+}
+
 blink::mojom::InputEventResultState RenderWidgetHostViewInput::FilterInputEvent(
     const blink::WebInputEvent& input_event) {
   // By default, input events are simply forwarded to the renderer.
diff --git a/components/input/render_widget_host_view_input.h b/components/input/render_widget_host_view_input.h
index db2e94f5..47509ea 100644
--- a/components/input/render_widget_host_view_input.h
+++ b/components/input/render_widget_host_view_input.h
@@ -153,6 +153,14 @@
       RenderWidgetHostViewInput* target_view,
       gfx::PointF* transformed_point);
 
+  // Returns true if |target_view| is one of |starting_view|'s ancestors.
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/content/browser/renderer_host/input/autoscroll_browsertest.cc b/content/browser/renderer_host/input/autoscroll_browsertest.cc
index baca3a93..128b62b 100644
--- a/content/browser/renderer_host/input/autoscroll_browsertest.cc
+++ b/content/browser/renderer_host/input/autoscroll_browsertest.cc
@@ -6,6 +6,7 @@
 
 #include "base/feature_list.h"
 #include "build/build_config.h"
+#include "components/input/render_widget_host_input_event_router.h"
 #include "content/browser/web_contents/web_contents_impl.h"
 #include "content/public/common/content_switches.h"
 #include "content/public/test/browser_test.h"
@@ -150,7 +151,14 @@
     down_event.button = blink::WebMouseEvent::Button::kMiddle;
     down_event.SetTimeStamp(ui::EventTimeForNow());
     down_event.SetPositionInScreen(x, y);
-    GetWidgetHost()->ForwardMouseEvent(down_event);
+
+    auto* router = GetWidgetHost()->delegate()->GetInputEventRouter();
+    if (router) {
+      router->RouteMouseEvent(GetWidgetHost()->GetView(), &down_event,
+                              ui::LatencyInfo());
+    } else {
+      GetWidgetHost()->ForwardMouseEvent(down_event);
+    }
 
     // Simulate and send middle click mouse up.
     blink::WebMouseEvent up_event = blink::SyntheticWebMouseEventBuilder::Build(
@@ -158,7 +166,13 @@
     up_event.button = blink::WebMouseEvent::Button::kMiddle;
     up_event.SetTimeStamp(ui::EventTimeForNow());
     up_event.SetPositionInScreen(x, y);
-    GetWidgetHost()->ForwardMouseEvent(up_event);
+
+    if (router) {
+      router->RouteMouseEvent(GetWidgetHost()->GetView(), &up_event,
+                              ui::LatencyInfo());
+    } else {
+      GetWidgetHost()->ForwardMouseEvent(up_event);
+    }
 
     // Wait till the IPC messages arrive and IsAutoscrollInProgress() toggles.
     while (GetWidgetHost()->IsAutoscrollInProgress() ==
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 27ad10f7..5a590ad 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
@@ -127,9 +127,16 @@
       const gfx::PointF& point,
       input::RenderWidgetHostViewInput* target_view,
       gfx::PointF* transformed_point) override {
+    if (target_view == this) {
+      *transformed_point = point;
+    } else {
+      *transformed_point = point - offset_;
+    }
     return true;
   }
 
+  void SetOffset(const gfx::Vector2dF& offset) { offset_ = offset; }
+
   void ProcessGestureEvent(const blink::WebGestureEvent& event,
                            const ui::LatencyInfo&) override {
     last_gesture_seen_ = event.GetType();
@@ -174,6 +181,7 @@
       blink::WebInputEvent::Type::kUndefined;
   uint32_t unique_id_for_last_touch_ack_ = 0;
   bool force_null_rir_ = false;
+  gfx::Vector2dF offset_;
 };
 
 class MockInputTargetClient : public viz::mojom::InputTargetClient {
@@ -1303,7 +1311,7 @@
       rwhier()->GetRenderWidgetTargeterForTests();
   rwhier()->RouteMouseEvent(view_root_.get(), &mouse_event, ui::LatencyInfo());
   // Set middle click autoscroll in progress to true.
-  rwhier()->SetAutoScrollInProgress(true);
+  rwhier()->SetAutoScrollInProgress(child.view.get(), true);
   // Destroy the view/target, middle click autoscroll is latched to.
   rwhier()->OnRenderWidgetHostViewInputDestroyed(child.view.get());
 
@@ -1914,6 +1922,89 @@
   view_root_->GetCursorManager()->ViewBeingDestroyed(child.view.get());
 }
 
+// This test reproduces the bug where autoscroll coordinate transformation is
+// bypassed, leading to incorrect coordinates being sent to the target view.
+TEST_F(RenderWidgetHostInputEventRouterTest,
+       AutoscrollCoordinateTransformation) {
+  ChildViewState child = MakeChildView(view_root_.get());
+  // Set an offset for the child view.
+  view_root_->SetOffset(gfx::Vector2dF(10, 10));
+
+  // 1. Simulate middle click mouse event on child view.
+  // This should populate middle_click_result_ in the targeter.
+  blink::WebMouseEvent middle_down_event(
+      blink::WebInputEvent::Type::kMouseDown,
+      blink::WebInputEvent::kNoModifiers,
+      blink::WebInputEvent::GetStaticTimeStampForTests());
+  middle_down_event.button = blink::WebPointerProperties::Button::kMiddle;
+  middle_down_event.SetPositionInWidget(50, 50);
+
+  // Set up hit testing to return the child view.
+  view_root_->SetHittestResult(child.view.get(), false);
+
+  rwhier()->RouteMouseEvent(view_root_.get(), &middle_down_event,
+                            ui::LatencyInfo());
+
+  // 2. Set autoscroll in progress to true.
+  rwhier()->SetAutoScrollInProgress(child.view.get(), true);
+
+  // 3. Send a left click mouse event at a different coordinate in the root
+  // view.
+  blink::WebMouseEvent left_down_event(
+      blink::WebInputEvent::Type::kMouseDown,
+      blink::WebInputEvent::kNoModifiers,
+      blink::WebInputEvent::GetStaticTimeStampForTests());
+  left_down_event.button = blink::WebPointerProperties::Button::kLeft;
+  // Use a coordinate that is clearly different from the middle click.
+  left_down_event.SetPositionInWidget(100, 100);
+
+  rwhier()->RouteMouseEvent(view_root_.get(), &left_down_event,
+                            ui::LatencyInfo());
+
+  // Verify that the event was routed to the child view.
+  EXPECT_EQ(child.view.get(), last_mouse_down_target());
+
+  // Verify the coordinates.
+  // With the fix, they should be transformed to (90, 90).
+  // Without the fix, they will be (100, 100).
+  EXPECT_EQ(gfx::PointF(90, 90),
+            rwhier()->mouse_down_post_transformed_coordinate_for_testing());
+
+  rwhier()->OnRenderWidgetHostViewInputDestroyed(child.view.get());
+}
+
+// This test verifies that autoscroll can only be started by the view that
+// received the middle click.
+TEST_F(RenderWidgetHostInputEventRouterTest, AutoscrollValidation) {
+  ChildViewState child = MakeChildView(view_root_.get());
+  ChildViewState other_child = MakeChildView(view_root_.get());
+
+  // 1. Middle click on child.
+  blink::WebMouseEvent middle_down_event(
+      blink::WebInputEvent::Type::kMouseDown,
+      blink::WebInputEvent::kNoModifiers,
+      blink::WebInputEvent::GetStaticTimeStampForTests());
+  middle_down_event.button = blink::WebPointerProperties::Button::kMiddle;
+  view_root_->SetHittestResult(child.view.get(), false);
+  rwhier()->RouteMouseEvent(view_root_.get(), &middle_down_event,
+                            ui::LatencyInfo());
+
+  // 2. Try to start autoscroll from other_child. Should be rejected.
+  rwhier()->SetAutoScrollInProgress(other_child.view.get(), true);
+  EXPECT_FALSE(rwhier()
+                   ->GetRenderWidgetTargeterForTests()
+                   ->is_auto_scroll_in_progress());
+
+  // 3. Start autoscroll from child. Should be accepted.
+  rwhier()->SetAutoScrollInProgress(child.view.get(), true);
+  EXPECT_TRUE(rwhier()
+                  ->GetRenderWidgetTargeterForTests()
+                  ->is_auto_scroll_in_progress());
+
+  rwhier()->OnRenderWidgetHostViewInputDestroyed(child.view.get());
+  rwhier()->OnRenderWidgetHostViewInputDestroyed(other_child.view.get());
+}
+
 #endif  // defined(USE_AURA)
 
 }  // namespace content
diff --git a/content/test/mock_render_widget_host_delegate.cc b/content/test/mock_render_widget_host_delegate.cc
index 0fcfe1f2..91d67af 100644
--- a/content/test/mock_render_widget_host_delegate.cc
+++ b/content/test/mock_render_widget_host_delegate.cc
@@ -116,4 +116,7 @@
   return nullptr;
 }
 
+void MockRenderWidgetHostDelegate::CancelAutoscroll(
+    input::RenderWidgetHostViewInput* view) {}
+
 }  // namespace content
diff --git a/content/test/mock_render_widget_host_delegate.h b/content/test/mock_render_widget_host_delegate.h
index d40090f..70eae0f 100644
--- a/content/test/mock_render_widget_host_delegate.h
+++ b/content/test/mock_render_widget_host_delegate.h
@@ -78,6 +78,7 @@
 
   //  RenderWidgetHostInputEventRouter::Delegate
   input::TouchEmulator* GetTouchEmulator(bool create_if_necessary) override;
+  void CancelAutoscroll(input::RenderWidgetHostViewInput* view) override;
 
  private:
   std::unique_ptr<input::NativeWebKeyboardEvent> last_event_;
Loading diff…

Original Bug Report

reported by [email protected]

Potential Cross-Origin Input Injection via Stale Autoscroll Target

Flapjack, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A logic flaw in RenderWidgetTargeter could potentially allow a compromised renderer to bypass Site Isolation by forcing the browser to route input events to a previously middle-clicked cross-origin frame. By triggering an autoscroll state via Mojo, the attacker can spoof event coordinates, leading to precise cross-origin input injection.

Affected files:

  • components/input/render_widget_targeter.cc
  • components/input/render_widget_targeter.h

Estimated timestamp from git blame: 2020-04-03

Summary

A potential logic vulnerability exists in RenderWidgetTargeter that could allow a compromised renderer process to perform cross-origin input injection, bypassing Site Isolation. By manipulating the global autoscroll state and exploiting a stale target cache, an attacker could force the browser to route mouse events (such as clicks) to an arbitrary cross-origin RenderWidgetHostViewInput that was previously middle-clicked by the user, while also spoofing the coordinates of these events.

Technical Details

The RenderWidgetTargeter class manages input event targeting within a WebContents. To support middle-click autoscrolling, it caches the initial middle-click target in the middle_click_result_ member and uses a flag is_autoscroll_in_progress_ to determine when to use this cached target.

Three key conditions combine to create this potential vulnerability:

  1. Stale Target Cache: When a user middle-clicks a view, it is stored in middle_click_result_ (components/input/render_widget_targeter.cc). When autoscroll ends (or if it never started but the user middle-clicked), the cache is only cleared under specific conditions. If an attacker triggers SetIsAutoScrollInProgress(true), the code specifically does not clear the cache (it only clears it on false).

  2. Mojo-Triggered Autoscroll State: A compromised renderer can invoke AutoscrollStart via the blink.mojom.WidgetHost Mojo interface. This sets is_autoscroll_in_progress_ = true globally for the RenderWidgetTargeter associated with the tab.

  3. Coordinate Transformation Failure: In RenderWidgetTargeter::ResolveTargetingRequest, when is_autoscroll_in_progress_ is true, the code bypasses standard hit-testing and resolves the target to the cached middle_click_result_. Crucially, it fails to transform the event’s coordinates from the root view’s space to the target view’s space:

    if (is_autoscroll_in_progress_) {
      result.target_location = request_target_location;
    }
    

    Because request_target_location holds the root view’s coordinates, assigning it directly to result.target_location dispatches the event to the cross-origin target view using the root view’s coordinate space.

Potential Exploitation Steps

Note: Our tooling agent does not have the ability to run code, so these are potential steps an attacker would follow.

  1. An attacker compromises a renderer process (e.g., via a separate V8 vulnerability).
  2. The compromised renderer hosts a page that embeds a sensitive cross-origin iframe (e.g., an account management page).
  3. The attacker tricks the user into middle-clicking anywhere on the cross-origin iframe (e.g., using a transparent overlay or a fake button). This populates the browser-side middle_click_result_ cache with the iframe’s RenderWidgetHostViewInput.
  4. The compromised renderer sends an AutoscrollStart IPC message via Mojo to the browser process, setting is_autoscroll_in_progress_ to true.
  5. The attacker tricks the user into left-clicking at a specific coordinate (X, Y) relative to the main window. This coordinate corresponds to the location of a sensitive element (like a “Delete Account” button) in the iframe’s local coordinate space.
  6. The browser routes the left-click. Because is_autoscroll_in_progress_ is true, it bypasses hit-testing, pulls the iframe from middle_click_result_, and forwards the click using the raw (X, Y) coordinates without translating them.
  7. The cross-origin iframe registers a click on the sensitive element, bypassing Site Isolation protections.

Suggested Fix

  1. State Validation: Ensure that RenderWidgetTargeter::SetIsAutoScrollInProgress(true) validates that a valid middle-click autoscroll sequence is genuinely active (e.g., initiated by the browser handling a middle click, rather than blindly trusting the renderer’s Mojo message).
  2. Coordinate Transformation: In RenderWidgetTargeter::ResolveTargetingRequest, ensure that if a cached target is used, request_target_location is properly transformed into the target view’s coordinate space using delegate_->FindTargetSynchronouslyAtPoint or similar coordinate-mapping utilities, rather than directly copying the root coordinates.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker