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
Tracker511742228
Fix commit1bdeaddc4e64 (chromium/src) +59/-20
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-27

Changed Functions

FunctionChangeNotes
if
components/input/render_widget_targeter.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
  • components/input/render_widget_host_input_event_router.h
  • components/input/render_widget_targeter.cc
  • components/input/render_widget_targeter.h
  • content/browser/renderer_host/render_widget_host_input_event_router_unittest.cc
From 1bdeaddc4e64382289ada2f423f678227f725c99 Mon Sep 17 00:00:00 2001
From: Aman Verma <[email protected]>
Date: Mon, 11 May 2026 08:44:25 -0700
Subject: [PATCH] [input] Validate FrameSinkId in async hit-test responses

The browser process failed to validate that the `FrameSinkId` returned
by the renderer in async hit-testing belongs to a valid descendant
frame. This could allow a compromised renderer to redirect input to
arbitrary cross-origin frames.

This CL adds a check to verify that the returned view is a descendant of
the queried target. If not, it falls back to the original target with
original coordinates.

Bug: 511742228
Change-Id: I8d6e258f14182dc3c7f1a2a46e5319c787ad4c56
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7829013
Reviewed-by: Jonathan Ross <[email protected]>
Commit-Queue: Aman Verma <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1628595}
---

diff --git a/components/input/render_widget_host_input_event_router.cc b/components/input/render_widget_host_input_event_router.cc
index 2fe4a4b2..f0939b21 100644
--- a/components/input/render_widget_host_input_event_router.cc
+++ b/components/input/render_widget_host_input_event_router.cc
@@ -1894,14 +1894,23 @@
 
 RenderWidgetHostViewInput*
 RenderWidgetHostInputEventRouter::FindViewFromFrameSinkId(
-    const viz::FrameSinkId& frame_sink_id) const {
+    const viz::FrameSinkId& frame_sink_id,
+    RenderWidgetHostViewInput* ancestor_to_verify) const {
   // TODO(kenrb): There should be a better way to handle hit tests to surfaces
   // that are no longer valid for hit testing. See https://crbug.com/790044.
   auto iter = owner_map_.find(frame_sink_id);
   // If the point hit a Surface whose namspace is no longer in the map, then
   // it likely means the RenderWidgetHostView has been destroyed but its
   // parent frame has not sent a new compositor frame since that happened.
-  return iter == owner_map_.end() ? nullptr : iter->second.get();
+  RenderWidgetHostViewInput* view =
+      iter == owner_map_.end() ? nullptr : iter->second.get();
+
+  if (view && ancestor_to_verify && view != ancestor_to_verify &&
+      !IsAncestorView(view, ancestor_to_verify)) {
+    return nullptr;
+  }
+
+  return view;
 }
 
 bool RenderWidgetHostInputEventRouter::ShouldContinueHitTesting(
diff --git a/components/input/render_widget_host_input_event_router.h b/components/input/render_widget_host_input_event_router.h
index b10dd416..143badd0 100644
--- a/components/input/render_widget_host_input_event_router.h
+++ b/components/input/render_widget_host_input_event_router.h
@@ -189,7 +189,8 @@
 
   // RenderWidgetTargeter::Delegate:
   RenderWidgetHostViewInput* FindViewFromFrameSinkId(
-      const viz::FrameSinkId& frame_sink_id) const override;
+      const viz::FrameSinkId& frame_sink_id,
+      RenderWidgetHostViewInput* ancestor_to_verify = nullptr) const override;
   bool ShouldContinueHitTesting(
       RenderWidgetHostViewInput* target_view) const override;
 
diff --git a/components/input/render_widget_targeter.cc b/components/input/render_widget_targeter.cc
index 9ca79ac..018b6d3c9 100644
--- a/components/input/render_widget_targeter.cc
+++ b/components/input/render_widget_targeter.cc
@@ -315,9 +315,9 @@
       delegate_->SetEventsBeingFlushed(true);
       events_being_flushed = true;
     }
-      ResolveTargetingRequest(std::move(request));
+    ResolveTargetingRequest(std::move(request));
   }
-    delegate_->SetEventsBeingFlushed(false);
+  delegate_->SetEventsBeingFlushed(false);
 }
 
 void RenderWidgetTargeter::FoundFrameSinkId(
@@ -348,16 +348,22 @@
       ->input_target_client()
       .set_disconnect_handler(base::OnceClosure());
 
-  auto* view = delegate_->FindViewFromFrameSinkId(frame_sink_id);
-  if (!view) {
-    view = target.get();
-  }
+  // Ensure the returned view is a valid descendant of the frame we queried
+  // (|target.get()|) to prevent a compromised renderer from redirecting input.
+  RenderWidgetHostViewInput* resolved_view =
+      delegate_->FindViewFromFrameSinkId(frame_sink_id, target.get());
+
+  // Compute final target and location.
+  RenderWidgetHostViewInput* final_view =
+      resolved_view ? resolved_view : target.get();
+  gfx::PointF final_location =
+      resolved_view ? transformed_location : target_location;
 
   // If a client returned an embedded target, then it might be necessary to
   // continue asking the clients until a client claims an event for itself.
-  if (view == target.get() ||
-      unresponsive_views_.find(view) != unresponsive_views_.end() ||
-      !delegate_->ShouldContinueHitTesting(view)) {
+  if (final_view == target.get() ||
+      unresponsive_views_.find(final_view) != unresponsive_views_.end() ||
+      !delegate_->ShouldContinueHitTesting(final_view)) {
     // Reduced scope is required since FoundTarget can trigger another query
     // which would end up linked to the current query.
     {
@@ -368,13 +374,13 @@
 
     if (request.IsWebInputEventRequest() &&
         IsMouseMiddleClick(*request.GetEvent())) {
-      middle_click_result_ = {view, /*should_query_view=*/false,
-                              transformed_location};
+      middle_click_result_ = {final_view, /*should_query_view=*/false,
+                              final_location};
     }
 
-    FoundTarget(view, transformed_location, &request);
+    FoundTarget(final_view, final_location, &request);
   } else {
-    QueryClient(view, transformed_location, target.get(), target_location,
+    QueryClient(final_view, final_location, target.get(), target_location,
                 std::move(request));
   }
 }
diff --git a/components/input/render_widget_targeter.h b/components/input/render_widget_targeter.h
index c2fa006b..c7e88957 100644
--- a/components/input/render_widget_targeter.h
+++ b/components/input/render_widget_targeter.h
@@ -75,7 +75,8 @@
     virtual void SetEventsBeingFlushed(bool events_being_flushed) = 0;
 
     virtual RenderWidgetHostViewInput* FindViewFromFrameSinkId(
-        const viz::FrameSinkId& frame_sink_id) const = 0;
+        const viz::FrameSinkId& frame_sink_id,
+        RenderWidgetHostViewInput* ancestor_to_verify = nullptr) const = 0;
 
     // Returns true if a further asynchronous query should be sent to the
     // candidate RenderWidgetHostView.
@@ -214,9 +215,8 @@
       base::WeakPtr<RenderWidgetHostViewInput> last_request_target,
       const gfx::PointF& last_target_location);
 
-  void OnInputTargetDisconnect(
-      base::WeakPtr<RenderWidgetHostViewInput> target,
-      const gfx::PointF& location);
+  void OnInputTargetDisconnect(base::WeakPtr<RenderWidgetHostViewInput> target,
+                               const gfx::PointF& location);
 
   HitTestResultsMatch GetHitTestResultsMatchBucket(
       RenderWidgetHostViewInput* target,
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 a30f26fe..ae0762fb 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
@@ -546,6 +546,29 @@
   EXPECT_EQ(view_root_->last_id_for_touch_ack(), 2lu);
 }
 
+// Tests that FindViewFromFrameSinkId correctly verifies the descendant
+// relationship when an expected ancestor is provided. It should return
+// nullptr if the found view is not a descendant of the expected ancestor.
+TEST_F(RenderWidgetHostInputEventRouterTest,
+       FindViewFromFrameSinkIdWithAncestorVerification) {
+  ChildViewState child1 = MakeChildView(view_root_.get());
+  ChildViewState child2 = MakeChildView(view_root_.get());
+
+  // child2 is NOT a descendant of child1.
+  EXPECT_EQ(nullptr, rwhier()->FindViewFromFrameSinkId(
+                         child2.view->GetFrameSinkId(), child1.view.get()));
+
+  // child2 IS a descendant of view_root_.
+  EXPECT_EQ(child2.view.get(),
+            rwhier()->FindViewFromFrameSinkId(child2.view->GetFrameSinkId(),
+                                              view_root_.get()));
+
+  // child1 IS a descendant of view_root_.
+  EXPECT_EQ(child1.view.get(),
+            rwhier()->FindViewFromFrameSinkId(child1.view->GetFrameSinkId(),
+                                              view_root_.get()));
+}
+
 TEST_F(RenderWidgetHostInputEventRouterTest, DoNotCoalesceTouchEvents) {
   // We require the presence of a child view, otherwise targeting is short
   // circuited.
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 a30f26fe..ae0762fb 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
@@ -546,6 +546,29 @@
   EXPECT_EQ(view_root_->last_id_for_touch_ack(), 2lu);
 }
 
+// Tests that FindViewFromFrameSinkId correctly verifies the descendant
+// relationship when an expected ancestor is provided. It should return
+// nullptr if the found view is not a descendant of the expected ancestor.
+TEST_F(RenderWidgetHostInputEventRouterTest,
+       FindViewFromFrameSinkIdWithAncestorVerification) {
+  ChildViewState child1 = MakeChildView(view_root_.get());
+  ChildViewState child2 = MakeChildView(view_root_.get());
+
+  // child2 is NOT a descendant of child1.
+  EXPECT_EQ(nullptr, rwhier()->FindViewFromFrameSinkId(
+                         child2.view->GetFrameSinkId(), child1.view.get()));
+
+  // child2 IS a descendant of view_root_.
+  EXPECT_EQ(child2.view.get(),
+            rwhier()->FindViewFromFrameSinkId(child2.view->GetFrameSinkId(),
+                                              view_root_.get()));
+
+  // child1 IS a descendant of view_root_.
+  EXPECT_EQ(child1.view.get(),
+            rwhier()->FindViewFromFrameSinkId(child1.view->GetFrameSinkId(),
+                                              view_root_.get()));
+}
+
 TEST_F(RenderWidgetHostInputEventRouterTest, DoNotCoalesceTouchEvents) {
   // We require the presence of a child view, otherwise targeting is short
   // circuited.
Loading diff…

Original Bug Report

reported by [email protected]

Site Isolation Bypass via Unvalidated FrameSinkId in Async Hit-Testing

Project Fortify, 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 https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: The browser process relies on the renderer to perform asynchronous hit-testing when synchronous compositor hit-testing is inconclusive. However, the browser fails to validate that the FrameSinkId returned by the renderer belongs to a valid descendant frame. A compromised renderer can exploit this to redirect user input (e.g., clicks) to any cross-origin frame within the same tab at arbitrary coordinates.

Affected files:

  • components/input/render_widget_targeter.cc
  • components/input/render_widget_host_input_event_router.cc

Estimated timestamp from git blame: 2026-01-13

Summary

A potential Site Isolation bypass exists in the browser’s input routing mechanism due to insufficient validation of hit-test targets returned by renderer processes. During asynchronous hit-testing, a compromised renderer can supply a forged FrameSinkId and coordinates via Mojo. The browser accepts this response and routes the user’s input event to the spoofed target. Because the lookup map (owner_map_) is flat per WebContents, an attacker can redirect legitimate user clicks to cross-origin sibling or ancestor iframes within the same tab, bypassing clickjacking protections.

Vulnerability Details

When the browser’s compositor (Viz) encounters a complex visual region (e.g., overlapping elements or complex CSS clip-paths), it cannot synchronously resolve the hit-test target and returns viz::HitTestRegionFlags::kHitTestAsk. This forces the browser to query the suspected renderer process asynchronously.

  1. RenderWidgetTargeter::QueryClient sends an InputTargetClient::FrameSinkIdAt Mojo request to the renderer.
  2. The renderer responds with a viz::FrameSinkId and a transformed_location (local coordinates).
  3. The browser handles this response in RenderWidgetTargeter::FoundFrameSinkId, which attempts to resolve the FrameSinkId to a view by calling delegate_->FindViewFromFrameSinkId.
  4. RenderWidgetHostInputEventRouter::FindViewFromFrameSinkId performs a raw lookup in owner_map_. This map contains all RenderWidgetHostViewInput instances for the entire WebContents (tab).
  5. The Flaw: Neither FoundFrameSinkId nor FindViewFromFrameSinkId verifies that the resolved view is an actual spatial or DOM descendant of the renderer that was queried.
  6. The browser subsequently invokes FoundTarget and DispatchEventToTarget, which calls DispatchMouseEvent.
  7. DispatchMouseEvent explicitly overrides the event’s widget coordinates with the attacker-provided location (event.SetPositionInWidget(target_location->x(), target_location->y());) and dispatches the genuine user event to the victim frame.

Since FrameSinkId is composed of a process ID and a sequentially allocated routing ID, it is trivially guessable or enumerable by a compromised renderer.

Impact

A compromised renderer can hijack legitimate user input events (clicks, touches) and redirect them to arbitrary cross-origin frames in the same tab. The attacker controls both the target frame and the exact local coordinates, allowing them to interact with sensitive cross-origin UIs (e.g., clicking “Authorize” buttons or submitting forms) as if they were the user.

Suggested Exploitation Steps (Potential)

Note: Our tooling agent does not have the ability to run code. These are potential steps based on static analysis.

  1. An attacker compromises a renderer process (e.g., via a separate V8 RCE) and hosts a webpage (attacker.com).
  2. The page embeds a cross-origin victim iframe (victim.com).
  3. The attacker applies complex CSS (e.g., clip-path) to a div on attacker.com where a user is likely to click, ensuring Viz returns kHitTestAsk.
  4. The user clicks the styled div.
  5. The browser sends a FrameSinkIdAt Mojo query to the compromised renderer.
  6. The attacker intercepts the Mojo request, guesses the FrameSinkId of the victim.com iframe, and returns it along with local coordinates pointing to a sensitive button within the victim frame.
  7. The browser process routes the genuine user click to the victim.com iframe at the spoofed coordinates.

Suggested Fix

The browser must validate the hierarchy of the returned hit-test target. In RenderWidgetTargeter::FoundFrameSinkId, after resolving the view via FindViewFromFrameSinkId, verify that the returned view is a valid descendant of the originally queried target view. If it is not a descendant (or the target itself), the response should be treated as invalid (e.g., discarded or defaulted back to the queried view).

Evaluated with Chrome root at commit: eca8648a4e1cdfdda68c495a6003059fed641955


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