CVE-2026-10938
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
whilecomponents/input/render_widget_host_input_event_router.cc |
modified | |
ifcomponents/input/render_widget_host_input_event_router.cc |
modified | |
whilecomponents/input/render_widget_host_view_input.cc |
modified | |
ifcomponents/input/render_widget_host_view_input.cc |
modified |
Files Changed
components/input/render_widget_host_input_event_router.cccomponents/input/render_widget_host_input_event_router.hcomponents/input/render_widget_host_view_input.cccomponents/input/render_widget_host_view_input.h
Patch
From 5b1580197b3cbe292f4b3a5cb3a68e19ed485fbf Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
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 <jonross@chromium.org>
Reviewed-by: Charlie Reis <creis@chromium.org>
Reviewed-by: Kartar Singh <kartarsingh@google.com>
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.
Regression Test / PoC
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_;
Original Bug Report
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.cccomponents/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:
-
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 triggersSetIsAutoScrollInProgress(true), the code specifically does not clear the cache (it only clears it onfalse). -
Mojo-Triggered Autoscroll State: A compromised renderer can invoke
AutoscrollStartvia theblink.mojom.WidgetHostMojo interface. This setsis_autoscroll_in_progress_ = trueglobally for theRenderWidgetTargeterassociated with the tab. -
Coordinate Transformation Failure: In
RenderWidgetTargeter::ResolveTargetingRequest, whenis_autoscroll_in_progress_is true, the code bypasses standard hit-testing and resolves the target to the cachedmiddle_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_locationholds the root view’s coordinates, assigning it directly toresult.target_locationdispatches 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.
- An attacker compromises a renderer process (e.g., via a separate V8 vulnerability).
- The compromised renderer hosts a page that embeds a sensitive cross-origin iframe (e.g., an account management page).
- 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’sRenderWidgetHostViewInput. - The compromised renderer sends an
AutoscrollStartIPC message via Mojo to the browser process, settingis_autoscroll_in_progress_totrue. - 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. - The browser routes the left-click. Because
is_autoscroll_in_progress_istrue, it bypasses hit-testing, pulls the iframe frommiddle_click_result_, and forwards the click using the raw(X, Y)coordinates without translating them. - The cross-origin iframe registers a click on the sensitive element, bypassing Site Isolation protections.
Suggested Fix
- 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). - Coordinate Transformation: In
RenderWidgetTargeter::ResolveTargetingRequest, ensure that if a cached target is used,request_target_locationis properly transformed into the target view’s coordinate space usingdelegate_->FindTargetSynchronouslyAtPointor 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.