CVE-2026-79272
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fcontent/browser/renderer_host/render_widget_host_unittest.cc |
modified |
Files Changed
content/browser/renderer_host/render_widget_host_impl.cccontent/browser/renderer_host/render_widget_host_unittest.cc
Patch
From 52346e7c98b42b77cdf54e69798efeabc8057735 Mon Sep 17 00:00:00 2001 From: Tzarial <[email protected]> Date: Wed, 15 Jul 2026 08:37:24 -0700 Subject: [PATCH] [agy][content] Clip zoom rect to view bounds Clip the zoom rect to the sender's view bounds in RenderWidgetHostImpl before transforming it to root view coordinates. This prevents a compromised renderer from using large rects to probe locations outside its own view bounds. Fixed: 516921259 Test: content_unittests --gtest_filter=RenderWidgetHostTest.*Clipped* Change-Id: I7e58b9ff6ffabf9ca75c596d2deeb2edf55cb7a6 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8085808 Commit-Queue: Tzarial <[email protected]> Reviewed-by: Bo Liu <[email protected]> Cr-Commit-Position: refs/heads/main@{#1662628} --- diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc index b15cf04c..eac793b 100644 --- a/content/browser/renderer_host/render_widget_host_impl.cc +++ b/content/browser/renderer_host/render_widget_host_impl.cc @@ -4249,15 +4249,18 @@ } gfx::Rect view_local_bounds(view_->GetViewBounds().size()); - if (!view_local_bounds.IsEmpty() && - (!view_local_bounds.Contains(point) || - !view_local_bounds.Intersects(rect_to_zoom))) { + if (!view_local_bounds.Contains(point)) { + return; + } + gfx::Rect clipped_rect_to_zoom(rect_to_zoom); + clipped_rect_to_zoom.Intersect(view_local_bounds); + if (clipped_rect_to_zoom.IsEmpty()) { return; } auto* root_view = view_->GetRootView(); gfx::Point transformed_point(point); - gfx::Rect transformed_rect_to_zoom(rect_to_zoom); + gfx::Rect transformed_rect_to_zoom(clipped_rect_to_zoom); if (!RenderWidgetHostViewBase::TransformPointAndRectToRootView( view_.get(), root_view, &transformed_point, &transformed_rect_to_zoom)) { @@ -4279,13 +4282,14 @@ } gfx::Rect view_local_bounds(view_->GetViewBounds().size()); - if (!view_local_bounds.IsEmpty() && - !view_local_bounds.Intersects(rect_to_zoom)) { + gfx::Rect clipped_rect_to_zoom(rect_to_zoom); + clipped_rect_to_zoom.Intersect(view_local_bounds); + if (clipped_rect_to_zoom.IsEmpty()) { return; } auto* root_view = view_->GetRootView(); - gfx::Rect transformed_rect_to_zoom(rect_to_zoom); + gfx::Rect transformed_rect_to_zoom(clipped_rect_to_zoom); if (!RenderWidgetHostViewBase::TransformPointAndRectToRootView( view_.get(), root_view, nullptr, &transformed_rect_to_zoom)) { return; diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc index d4ea399c..292316f6 100644 --- a/content/browser/renderer_host/render_widget_host_unittest.cc +++ b/content/browser/renderer_host/render_widget_host_unittest.cc @@ -2848,6 +2848,48 @@ ->ZoomToFindInPageRectInMainFrame(valid_rect); } +TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectClippedToViewBounds) { + view_->SetBounds(gfx::Rect(0, 0, 200, 200)); + + // Rect that overlaps the view bounds but extends well beyond them. The + // forwarded rect must be clipped so that no part of it (including its + // center) lies outside the sender's view. + gfx::Rect overlapping_rect(-1800, -10, 3900, 3900); + + EXPECT_CALL(mock_owner_delegate_, + ZoomToFindInPageRect(gfx::Rect(0, 0, 200, 200))) + .Times(1); + + static_cast<blink::mojom::FrameWidgetHost*>(host_.get()) + ->ZoomToFindInPageRectInMainFrame(overlapping_rect); +} + +TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectPartiallyClipped) { + view_->SetBounds(gfx::Rect(0, 0, 200, 200)); + + // Rect that partially overlaps the view bounds; the forwarded rect should + // be the intersection with the view bounds. + gfx::Rect partial_rect(150, 150, 100, 100); + + EXPECT_CALL(mock_owner_delegate_, + ZoomToFindInPageRect(gfx::Rect(150, 150, 50, 50))) + .Times(1); + + static_cast<blink::mojom::FrameWidgetHost*>(host_.get()) + ->ZoomToFindInPageRectInMainFrame(partial_rect); +} + +TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectEmptyBounds) { + view_->SetBounds(gfx::Rect(0, 0, 0, 0)); + + gfx::Rect valid_rect(10, 10, 5, 5); + + EXPECT_CALL(mock_owner_delegate_, ZoomToFindInPageRect(_)).Times(0); + + static_cast<blink::mojom::FrameWidgetHost*>(host_.get()) + ->ZoomToFindInPageRectInMainFrame(valid_rect); +} + TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomBoundsCheck) { view_->SetBounds(gfx::Rect(0, 0, 200, 200)); @@ -2879,4 +2921,33 @@ ->AnimateDoubleTapZoomInMainFrame(tap_point, valid_rect); } +TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomRectClippedToViewBounds) { + view_->SetBounds(gfx::Rect(0, 0, 200, 200)); + + // Rect that overlaps the view bounds but extends beyond them. The forwarded + // rect must be clipped to the sender's view bounds. + gfx::Rect overlapping_rect(150, 150, 100, 100); + gfx::Point tap_point(160, 160); + + EXPECT_CALL( + mock_owner_delegate_, + AnimateDoubleTapZoom(gfx::Point(160, 160), gfx::Rect(150, 150, 50, 50))) + .Times(1); + + static_cast<blink::mojom::FrameWidgetHost*>(host_.get()) + ->AnimateDoubleTapZoomInMainFrame(tap_point, overlapping_rect); +} + +TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomEmptyBounds) { + view_->SetBounds(gfx::Rect(0, 0, 0, 0)); + + gfx::Rect valid_rect(10, 10, 5, 5); + gfx::Point tap_point(12, 12); + + EXPECT_CALL(mock_owner_delegate_, AnimateDoubleTapZoom(_, _)).Times(0); + + static_cast<blink::mojom::FrameWidgetHost*>(host_.get()) + ->AnimateDoubleTapZoomInMainFrame(tap_point, valid_rect); +} + } // namespace content
Regression Test / PoC
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index d4ea399c..292316f6 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -2848,6 +2848,48 @@
->ZoomToFindInPageRectInMainFrame(valid_rect);
}
+TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectClippedToViewBounds) {
+ view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+ // Rect that overlaps the view bounds but extends well beyond them. The
+ // forwarded rect must be clipped so that no part of it (including its
+ // center) lies outside the sender's view.
+ gfx::Rect overlapping_rect(-1800, -10, 3900, 3900);
+
+ EXPECT_CALL(mock_owner_delegate_,
+ ZoomToFindInPageRect(gfx::Rect(0, 0, 200, 200)))
+ .Times(1);
+
+ static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+ ->ZoomToFindInPageRectInMainFrame(overlapping_rect);
+}
+
+TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectPartiallyClipped) {
+ view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+ // Rect that partially overlaps the view bounds; the forwarded rect should
+ // be the intersection with the view bounds.
+ gfx::Rect partial_rect(150, 150, 100, 100);
+
+ EXPECT_CALL(mock_owner_delegate_,
+ ZoomToFindInPageRect(gfx::Rect(150, 150, 50, 50)))
+ .Times(1);
+
+ static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+ ->ZoomToFindInPageRectInMainFrame(partial_rect);
+}
+
+TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectEmptyBounds) {
+ view_->SetBounds(gfx::Rect(0, 0, 0, 0));
+
+ gfx::Rect valid_rect(10, 10, 5, 5);
+
+ EXPECT_CALL(mock_owner_delegate_, ZoomToFindInPageRect(_)).Times(0);
+
+ static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+ ->ZoomToFindInPageRectInMainFrame(valid_rect);
+}
+
TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomBoundsCheck) {
view_->SetBounds(gfx::Rect(0, 0, 200, 200));
@@ -2879,4 +2921,33 @@
->AnimateDoubleTapZoomInMainFrame(tap_point, valid_rect);
}
+TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomRectClippedToViewBounds) {
+ view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+ // Rect that overlaps the view bounds but extends beyond them. The forwarded
+ // rect must be clipped to the sender's view bounds.
+ gfx::Rect overlapping_rect(150, 150, 100, 100);
+ gfx::Point tap_point(160, 160);
+
+ EXPECT_CALL(
+ mock_owner_delegate_,
+ AnimateDoubleTapZoom(gfx::Point(160, 160), gfx::Rect(150, 150, 50, 50)))
+ .Times(1);
+
+ static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+ ->AnimateDoubleTapZoomInMainFrame(tap_point, overlapping_rect);
+}
+
+TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomEmptyBounds) {
+ view_->SetBounds(gfx::Rect(0, 0, 0, 0));
+
+ gfx::Rect valid_rect(10, 10, 5, 5);
+ gfx::Point tap_point(12, 12);
+
+ EXPECT_CALL(mock_owner_delegate_, AnimateDoubleTapZoom(_, _)).Times(0);
+
+ static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+ ->AnimateDoubleTapZoomInMainFrame(tap_point, valid_rect);
+}
+
} // namespace content
Original Bug Report
DOM layout leak via unconstrained ZoomToFindInPageRectInMainFrame validation
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: An incomplete validation check in RenderWidgetHostImpl::ZoomToFindInPageRectInMainFrame may allow a compromised cross-origin renderer to bypass viewport bounds checks. By providing a specifically sized, extremely large rectangle, the renderer can pass the Intersects validation check while placing its center point at arbitrary coordinates in the parent frame. This can potentially leak cross-origin layout presence and dimensions through observable page scale or scroll changes.
Affected files:
content/browser/renderer_host/render_widget_host_impl.cc
Estimated timestamp from git blame: 2026-04-09
Root Cause Analysis
In RenderWidgetHostImpl::ZoomToFindInPageRectInMainFrame (located in content/browser/renderer_host/render_widget_host_impl.cc), the browser validates that the renderer-supplied rect_to_zoom overlaps with the sender’s view bounds to prevent cross-origin hit-testing:
void RenderWidgetHostImpl::ZoomToFindInPageRectInMainFrame(
const gfx::Rect& rect_to_zoom) {
if (!view_) {
return;
}
gfx::Rect view_local_bounds(view_->GetViewBounds().size());
if (!view_local_bounds.IsEmpty() &&
!view_local_bounds.Intersects(rect_to_zoom)) {
return;
}
...
However, the validation only ensures that the rect intersects the view bounds. The downstream hit-test in the main-frame renderer (WebViewImpl::ZoomToFindInPageRect in third_party/blink/renderer/core/exported/web_view_impl.cc) uses the center point of the transformed rect, rather than the entire rect bounds.
By supplying an extremely large rect that overlaps its own local view bounds (e.g., at the subframe’s local origin (0,0)) but has its center point placed at arbitrary main-frame coordinates, a compromised subframe renderer can bypass this check. The unclipped rect is transformed to the root view and forwarded directly to the main-frame renderer, leaving the target center point intact.
Potential Downstream Execution & Oracle Mechanism
Once the main frame receives the request, it executes a hit-test via WebViewImpl::ZoomToFindInPageRect:
void WebViewImpl::ZoomToFindInPageRect(const gfx::Rect& rect_in_root_frame) {
DCHECK(MainFrameImpl());
gfx::Rect block_bounds =
MainFrameImpl()->FrameWidgetImpl()->ComputeBlockBound(
gfx::Point(rect_in_root_frame.x() + rect_in_root_frame.width() / 2,
rect_in_root_frame.y() + rect_in_root_frame.height() / 2),
true); // ignore_clipping = true
...
Since ignore_clipping is true, the hit-test ignores visual viewport restrictions. A subframe could potentially probe the main frame using these steps:
- Calculate target coordinates $(P_x, P_y)$ in the main frame.
- Construct an unconstrained
rect_to_zoomsuch that it contains the subframe’s own local origin but centers at the targeted coordinates. - Send the Mojo IPC message
ZoomToFindInPageRectInMainFrame(rect_to_zoom)to the browser. - Observe whether a layout zoom or scroll animation is triggered (e.g., via
IntersectionObserveror resizing events in the subframe) to infer the presence and size of containing block elements at the probed coordinates.
Note: These steps are based on static analysis of the coordinate transformations and validation paths; our automated security analysis tools do not currently have the capability to execute interactive exploitation code to confirm runtime behavior.
Suggested Remediation
To address this potential leak, the browser process should constrain the sender-provided rect to its local view bounds before transformation. This can be achieved by explicitly intersecting or clipping rect_to_zoom with view_local_bounds, or by verifying that the rect is fully contained within the view bounds using Contains() rather than Intersects():
if (!view_local_bounds.IsEmpty() &&
!view_local_bounds.Contains(rect_to_zoom)) {
return;
}
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.