CVE-2026-79248
Overview
Background
- `RenderWidgetHostInputEventRouter`
- the browser-side component that hit-tests and routes input events among the
RenderWidgetHostViews of a singleWebContents, and is shared across thatWebContents’ frame trees. - Inner frame tree
- a nested frame tree (fenced frame, guest view) that shares the outer
WebContents’ input event router but is a separate, less-trusted embedding context. - `root_view_receive_additional_mouse_up_`
- a router flag that, when set, additionally re-dispatches a
kMouseUpto the outermost root view, used for main-frame autoscroll selection. - Primary frame tree
- the outermost main frame’s tree;
FrameTree::is_primary()distinguishes it from inner frame trees.
Root Cause Analysis
SetAutoscrollSelectionActiveInMainFrame set the router state (root_view_receive_additional_mouse_up_) that causes a kMouseUp to be additionally re-dispatched to the outermost root view, but it never verified that the caller was the outermost main frame. Because inner frame trees (fenced frames, guest views) share the outer WebContents’ input event router, a main-frame widget inside an inner tree could invoke this and cause mouse-up events to be routed to the outermost root view — an authorization-boundary violation in which less-trusted embedded content influences input delivery to the primary frame.
The fix adds an early return when !frame_tree_ || !frame_tree_->is_primary(), so only the outermost main frame can arm the behavior. Separately, the router’s additional re-dispatch was corrected to forward the original mouse_event instead of the transformed event, so the root view receives the original widget coordinates (the test expects the primary to see (10,20), not the target-local (5,8)).
SetAutoscrollSelectionActiveInMainFrame on frame_tree_->is_primary().Attack Path
- Embed an inner frame tree
the attacker page embeds a fenced frame (or guest view) whose main-frame widget shares the outer
WebContents’ input event router. - Arm the flag from the inner frame
the inner main-frame widget calls
SetAutoscrollSelectionActiveInMainFrame(true), which pre-patch setroot_view_receive_additional_mouse_up_on the shared router. - Trigger a mouse-up
a
kMouseUpdispatched to the inner frame is then additionally re-dispatched by the router to the outermost root view, delivering input the inner frame should not control. - Abuse outer input delivery
the spurious mouse-up to the outermost main frame could drive selection/autoscroll behavior in the primary frame
(inferred), a cross-context input effect.
Impact Assessment
WebContents, influencing input handling across a security boundary in the browser process; concrete consequences beyond spurious input routing are (inferred). The precondition is that the attacker controls content inside an inner frame tree that shares the outer input event router.Files Changed
content/browser/fenced_frame/fenced_frame_browsertest.cccontent/browser/renderer_host/render_widget_host_impl.cccomponents/input/render_widget_host_input_event_router.cccomponents/input/render_widget_host_input_event_router.h
Audit Directions
- Shared-router state set per-widgetaudit setters on
RenderWidgetHostInputEventRouterandRenderWidgetHostImplthat mutate router-wide state but are reachable from inner frame trees; each should verify the caller is the primary/outermost frame. - Inner-frame-tree authorizationfor fenced frames and guest views sharing an outer
WebContents, check every path where an inner main frame is treated like “the main frame” for privileges it should not have. - Event coordinate provenancereview re-dispatch sites that forward an event object to a different view to ensure the correct (original vs. transformed) coordinate space is used.
Patch
From 8de7cce11c3f5b4d9bc52ea323b795288369db99 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> Date: Fri, 17 Jul 2026 16:13:16 -0700 Subject: [PATCH] [agy][content] Restrict autoscroll to outermost Restrict SetAutoscrollSelectionActiveInMainFrame to only be honored when called from the outermost main frame. Inner main frames (like fenced frames or guest views) share the outer WebContents' input event router but should not be allowed to trigger mouse-up routing to the outermost root view. Fixed: 502918844 Test: FencedFrameMPArchBrowserTest.AutoscrollSelectionFromFencedFrameIgnored Change-Id: I199e18eb9e0da24da1e27cf6a0c436c6b9faba0a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8086442 Commit-Queue: Tzarial <[email protected]> Reviewed-by: Bo Liu <[email protected]> Reviewed-by: Kartar Singh <[email protected]> Cr-Commit-Position: refs/heads/main@{#1664233} --- diff --git a/components/input/render_widget_host_input_event_router.cc b/components/input/render_widget_host_input_event_router.cc index 3490fab..3595a0b 100644 --- a/components/input/render_widget_host_input_event_router.cc +++ b/components/input/render_widget_host_input_event_router.cc @@ -730,7 +730,7 @@ if (root_view_receive_additional_mouse_up_ && target != root_view && mouse_event.GetType() == blink::WebInputEvent::Type::kMouseUp) { - root_view->ProcessMouseEvent(event, latency); + root_view->ProcessMouseEvent(mouse_event, latency); } } diff --git a/components/input/render_widget_host_input_event_router.h b/components/input/render_widget_host_input_event_router.h index 69510dc5..64f38be 100644 --- a/components/input/render_widget_host_input_event_router.h +++ b/components/input/render_widget_host_input_event_router.h @@ -55,6 +55,8 @@ FORWARD_DECLARE_TEST( WebContentsImplBrowserTest, MouseUpInOOPIframeShouldCancelMainFrameAutoscrollSelection); +FORWARD_DECLARE_TEST(FencedFrameMPArchBrowserTest, + AutoscrollSelectionFromFencedFrameIgnored); FORWARD_DECLARE_TEST(SitePerProcessHitTestBrowserTest, CacheCoordinateTransformUponMouseDown); FORWARD_DECLARE_TEST(SitePerProcessHitTestBrowserTest, @@ -266,6 +268,8 @@ FRIEND_TEST_ALL_PREFIXES( content::WebContentsImplBrowserTest, MouseUpInOOPIframeShouldCancelMainFrameAutoscrollSelection); + FRIEND_TEST_ALL_PREFIXES(content::FencedFrameMPArchBrowserTest, + AutoscrollSelectionFromFencedFrameIgnored); using FrameSinkIdOwnerMap = std::unordered_map<viz::FrameSinkId, diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc index 189a571..9f7e49d 100644 --- a/content/browser/renderer_host/render_widget_host_impl.cc +++ b/content/browser/renderer_host/render_widget_host_impl.cc @@ -3517,6 +3517,14 @@ return; } + // Only the outermost main frame should request this. Main frames of inner + // frame trees (e.g. fenced frames, guest views) share the outer WebContents' + // input event router, but should not be allowed to trigger mouse-up routing + // to the outermost root view. + if (!frame_tree_ || !frame_tree_->is_primary()) { + return; + } + if (!delegate_ || !delegate_->GetInputEventRouter()) { return; } diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc index c6163d6..5d450e59 100644 --- a/content/browser/fenced_frame/fenced_frame_browsertest.cc +++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc @@ -21,6 +21,7 @@ #include "base/time/time.h" #include "build/build_config.h" #include "build/buildflag.h" +#include "components/input/render_widget_host_input_event_router.h" #include "components/network_session_configurator/common/network_switches.h" #include "components/ukm/test_ukm_recorder.h" #include "content/browser/back_forward_cache_browsertest.h" @@ -31,6 +32,8 @@ #include "content/browser/renderer_host/navigation_request.h" #include "content/browser/renderer_host/render_frame_host_impl.h" #include "content/browser/renderer_host/render_frame_proxy_host.h" +#include "content/browser/renderer_host/render_widget_host_impl.h" +#include "content/browser/renderer_host/render_widget_host_view_base.h" #include "content/browser/web_contents/web_contents_impl.h" #include "content/common/features.h" #include "content/public/browser/browser_context.h" @@ -1118,6 +1121,79 @@ fenced_frame_rfh->GetRenderWidgetHost()->ForwardMouseEvent(mouse_event); } +// Tests that a SetAutoscrollSelectionActiveInMainFrame request from a fenced +// frame's main-frame widget does not affect mouse-up routing to the outer +// root view. This should only be honored for the outermost main frame. +IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest, + AutoscrollSelectionFromFencedFrameIgnored) { + ASSERT_TRUE(https_server()->Start()); + const GURL main_url = https_server()->GetURL("c.test", "/title1.html"); + EXPECT_TRUE(NavigateToURL(shell(), main_url)); + RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host()); + + const GURL fenced_frame_url = + https_server()->GetURL("c.test", "/fenced_frames/title1.html"); + RenderFrameHostImplWrapper fenced_frame_rfh( + fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(), + fenced_frame_url)); + + RenderWidgetHostImpl* fenced_frame_rwh = + fenced_frame_rfh->GetRenderWidgetHost(); + RenderWidgetHostImpl* primary_rwh = primary_rfh->GetRenderWidgetHost(); + + ASSERT_TRUE(fenced_frame_rwh->owner_delegate()); + ASSERT_TRUE(fenced_frame_rwh->GetView()->IsRenderWidgetHostViewChildFrame()); + + input::RenderWidgetHostInputEventRouter* router = + web_contents()->GetInputEventRouter(); + + RenderWidgetHostMouseEventMonitor primary_monitor(primary_rwh); + RenderWidgetHostMouseEventMonitor fenced_frame_monitor(fenced_frame_rwh); + + // 1. Simulate the request arriving from the fenced frame's renderer. + // It should be ignored. + fenced_frame_rwh->SetAutoscrollSelectionActiveInMainFrame(true); + + // Dispatch MouseUp to fenced frame. + blink::WebMouseEvent mouse_up( + blink::WebInputEvent::Type::kMouseUp, blink::WebInputEvent::kNoModifiers, + blink::WebInputEvent::GetStaticTimeStampForTests()); + mouse_up.button = blink::WebPointerProperties::Button::kLeft; + mouse_up.SetPositionInWidget(10, 20); + gfx::PointF target_location(5, 8); + + router->DispatchMouseEvent(primary_rwh->GetView(), + fenced_frame_rwh->GetView(), mouse_up, + ui::LatencyInfo(), target_location); + + // Fenced frame should receive it at target_location. + EXPECT_TRUE(fenced_frame_monitor.EventWasReceived()); + EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().x(), 5); + EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().y(), 8); + // Primary main frame should NOT receive it. + EXPECT_FALSE(primary_monitor.EventWasReceived()); + + // Reset monitors. + fenced_frame_monitor.ResetEventReceived(); + primary_monitor.ResetEventReceived(); + + // 2. The outermost main frame's request should still be honored. + primary_rwh->SetAutoscrollSelectionActiveInMainFrame(true); + + router->DispatchMouseEvent(primary_rwh->GetView(), + fenced_frame_rwh->GetView(), mouse_up, + ui::LatencyInfo(), target_location); + + // Fenced frame should receive it at target_location. + EXPECT_TRUE(fenced_frame_monitor.EventWasReceived()); + EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().x(), 5); + EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().y(), 8); + // Primary main frame should ALSO receive it, but at original coordinates. + EXPECT_TRUE(primary_monitor.EventWasReceived()); + EXPECT_EQ(primary_monitor.event().PositionInWidget().x(), 10); + EXPECT_EQ(primary_monitor.event().PositionInWidget().y(), 20); +} + // Test that WebContents::GetFocusedFrame includes results from a fenced // frame's frame tree. IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest,
Regression Test / PoC
diff --git a/content/browser/fenced_frame/fenced_frame_browsertest.cc b/content/browser/fenced_frame/fenced_frame_browsertest.cc
index c6163d6..5d450e59 100644
--- a/content/browser/fenced_frame/fenced_frame_browsertest.cc
+++ b/content/browser/fenced_frame/fenced_frame_browsertest.cc
@@ -21,6 +21,7 @@
#include "base/time/time.h"
#include "build/build_config.h"
#include "build/buildflag.h"
+#include "components/input/render_widget_host_input_event_router.h"
#include "components/network_session_configurator/common/network_switches.h"
#include "components/ukm/test_ukm_recorder.h"
#include "content/browser/back_forward_cache_browsertest.h"
@@ -31,6 +32,8 @@
#include "content/browser/renderer_host/navigation_request.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/browser/renderer_host/render_frame_proxy_host.h"
+#include "content/browser/renderer_host/render_widget_host_impl.h"
+#include "content/browser/renderer_host/render_widget_host_view_base.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/features.h"
#include "content/public/browser/browser_context.h"
@@ -1118,6 +1121,79 @@
fenced_frame_rfh->GetRenderWidgetHost()->ForwardMouseEvent(mouse_event);
}
+// Tests that a SetAutoscrollSelectionActiveInMainFrame request from a fenced
+// frame's main-frame widget does not affect mouse-up routing to the outer
+// root view. This should only be honored for the outermost main frame.
+IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest,
+ AutoscrollSelectionFromFencedFrameIgnored) {
+ ASSERT_TRUE(https_server()->Start());
+ const GURL main_url = https_server()->GetURL("c.test", "/title1.html");
+ EXPECT_TRUE(NavigateToURL(shell(), main_url));
+ RenderFrameHostImplWrapper primary_rfh(primary_main_frame_host());
+
+ const GURL fenced_frame_url =
+ https_server()->GetURL("c.test", "/fenced_frames/title1.html");
+ RenderFrameHostImplWrapper fenced_frame_rfh(
+ fenced_frame_test_helper().CreateFencedFrame(primary_rfh.get(),
+ fenced_frame_url));
+
+ RenderWidgetHostImpl* fenced_frame_rwh =
+ fenced_frame_rfh->GetRenderWidgetHost();
+ RenderWidgetHostImpl* primary_rwh = primary_rfh->GetRenderWidgetHost();
+
+ ASSERT_TRUE(fenced_frame_rwh->owner_delegate());
+ ASSERT_TRUE(fenced_frame_rwh->GetView()->IsRenderWidgetHostViewChildFrame());
+
+ input::RenderWidgetHostInputEventRouter* router =
+ web_contents()->GetInputEventRouter();
+
+ RenderWidgetHostMouseEventMonitor primary_monitor(primary_rwh);
+ RenderWidgetHostMouseEventMonitor fenced_frame_monitor(fenced_frame_rwh);
+
+ // 1. Simulate the request arriving from the fenced frame's renderer.
+ // It should be ignored.
+ fenced_frame_rwh->SetAutoscrollSelectionActiveInMainFrame(true);
+
+ // Dispatch MouseUp to fenced frame.
+ blink::WebMouseEvent mouse_up(
+ blink::WebInputEvent::Type::kMouseUp, blink::WebInputEvent::kNoModifiers,
+ blink::WebInputEvent::GetStaticTimeStampForTests());
+ mouse_up.button = blink::WebPointerProperties::Button::kLeft;
+ mouse_up.SetPositionInWidget(10, 20);
+ gfx::PointF target_location(5, 8);
+
+ router->DispatchMouseEvent(primary_rwh->GetView(),
+ fenced_frame_rwh->GetView(), mouse_up,
+ ui::LatencyInfo(), target_location);
+
+ // Fenced frame should receive it at target_location.
+ EXPECT_TRUE(fenced_frame_monitor.EventWasReceived());
+ EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().x(), 5);
+ EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().y(), 8);
+ // Primary main frame should NOT receive it.
+ EXPECT_FALSE(primary_monitor.EventWasReceived());
+
+ // Reset monitors.
+ fenced_frame_monitor.ResetEventReceived();
+ primary_monitor.ResetEventReceived();
+
+ // 2. The outermost main frame's request should still be honored.
+ primary_rwh->SetAutoscrollSelectionActiveInMainFrame(true);
+
+ router->DispatchMouseEvent(primary_rwh->GetView(),
+ fenced_frame_rwh->GetView(), mouse_up,
+ ui::LatencyInfo(), target_location);
+
+ // Fenced frame should receive it at target_location.
+ EXPECT_TRUE(fenced_frame_monitor.EventWasReceived());
+ EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().x(), 5);
+ EXPECT_EQ(fenced_frame_monitor.event().PositionInWidget().y(), 8);
+ // Primary main frame should ALSO receive it, but at original coordinates.
+ EXPECT_TRUE(primary_monitor.EventWasReceived());
+ EXPECT_EQ(primary_monitor.event().PositionInWidget().x(), 10);
+ EXPECT_EQ(primary_monitor.event().PositionInWidget().y(), 20);
+}
+
// Test that WebContents::GetFocusedFrame includes results from a fenced
// frame's frame tree.
IN_PROC_BROWSER_TEST_F(FencedFrameMPArchBrowserTest,
Original Bug Report
Event Spoofing: Subframes can inject cross-origin MouseUp events into embedder via IPC
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 go/chrome-ai-generated-security-bugs-faq for more information.
Overview: A logic flaw in input event routing allows Fenced Frames and Guest Views to bypass main-frame restrictions and enable autoscroll event duplication. When enabled, a MouseUp event targeting the subframe is duplicated to the embedder without reverse coordinate transformation. This allows a compromised subframe to trigger onmouseup handlers at attacker-controlled coordinates in the cross-origin embedder.
Affected files:
components/input/render_widget_host_input_event_router.cccontent/browser/renderer_host/render_widget_host_impl.cc
Estimated timestamp from git blame: 2023-12-14
Summary
A potential logic vulnerability exists in the browser’s input event routing mechanism that allows a compromised renderer (specifically Guest main frames or Fenced Frame main frames) to inject cross-origin MouseUp events into its embedder. This occurs due to insufficient caller validation in RenderWidgetHostImpl::SetAutoscrollSelectionActiveInMainFrame and a coordinate transformation bug in RenderWidgetHostInputEventRouter::DispatchMouseEvent.
Technical Details
When a renderer performs an autoscroll-for-selection operation, it notifies the browser to duplicate subsequent MouseUp events to the root view. This ensures the top-level page knows the mouse was released even if it happens outside the scrolling iframe. The notification is sent via the WidgetInputHandlerHost::SetAutoscrollSelectionActiveInMainFrame Mojo message, handled by RenderWidgetHostImpl::SetAutoscrollSelectionActiveInMainFrame.
// content/browser/renderer_host/render_widget_host_impl.cc
void RenderWidgetHostImpl::SetAutoscrollSelectionActiveInMainFrame(
bool autoscroll_selection) {
// If there is no |owner_delegate|, this is not a main frame.
if (!owner_delegate()) {
mojo::ReportBadMessage(...);
return;
}
// ...
delegate_->GetInputEventRouter()->RootViewReceivesMouseUpIfNecessary(
autoscroll_selection);
}
The if (!owner_delegate()) check is intended to restrict this call to the outermost main frame. However, Guest main frames (e.g., <webview>) and Fenced Frame main frames possess their own FrameTree and RenderViewHostImpl, meaning they have a non-null owner_delegate_. This allows a compromised subframe renderer to bypass the check and set the root_view_receive_additional_mouse_up_ flag on the shared RenderWidgetHostInputEventRouter.
When a MouseUp event subsequently occurs within the compromised frame, the router duplicates the event to the root view:
// components/input/render_widget_host_input_event_router.cc
void RenderWidgetHostInputEventRouter::DispatchMouseEvent(...) {
// ...
blink::WebMouseEvent event = mouse_event;
event.SetPositionInWidget(target_location->x(), target_location->y());
target->ProcessMouseEvent(event, latency);
if (root_view_receive_additional_mouse_up_ && target != root_view &&
mouse_event.GetType() == blink::WebInputEvent::Type::kMouseUp) {
root_view->ProcessMouseEvent(event, latency);
}
}
Crucially, the event passed to root_view->ProcessMouseEvent at line 715 has already been transformed into the target subframe’s local coordinate space at line 697. Because the root_view (embedder) interprets these coordinates relative to its own top-left origin, the compromised renderer can inject MouseUp events at attacker-controlled positions within the embedder. The injectible coordinates are bounded by (0 <= x <= subframe_width, 0 <= y <= subframe_height).
Potential Attack Steps
- An attacker compromises a Fenced Frame or Guest View renderer process embedded on a target site.
- The compromised renderer sends the
SetAutoscrollSelectionActiveInMainFrame(true)Mojo IPC. - The attacker positions UI elements in their subframe to entice a user click at specific local coordinates
(x, y). - When the user clicks and releases the mouse over the subframe, the browser routes the
MouseUpto the subframe normally. - Due to the manipulated flag, the browser also routes a
MouseUpevent to the embedder. - The embedder’s renderer receives a
MouseUpat coordinates(x, y)relative to its own origin, potentially triggering sensitiveonmouseupevent handlers. (Note: A full DOMclickevent is typically not synthesized by Blink because the embedder never received the correspondingMouseDown, limiting the impact to raw pointer/mouse release handlers.)
Suggested Fix
Validation should be tightened to ensure only the primary main frame can enable this state. A robust check would be to verify that the requesting widget is not for a child frame:
if (!owner_delegate() || GetView()->IsRenderWidgetHostViewChildFrame()) {
mojo::ReportBadMessage(...);
return;
}
Alternatively, delegate_->IsWidgetForPrimaryMainFrame(this) could be used if appropriate.
Additionally, DispatchMouseEvent should be fixed so that if an event must be duplicated to the root view, it uses coordinates transformed into the root view’s coordinate space (or simply uses the original mouse_event which is already in root coordinates), rather than the target’s local coordinates.
Evaluated with Chrome root at commit: 661452647ddb2827305122ff3273bd5dea403f09
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.