Medium watch CVSS 3.1 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactImproper input validation in FindInPage
DescriptionImproper input validation in FindInPage
ComponentFindInPage
Sub-componentContent
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker516921259
Fix commit52346e7c98b4 (chromium/src) +82/-7
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-27 (per vendor advisory)
Fix landed2026-07-15 (public git)
Disclosed2026-08-25
Patch gap41 days — fix public in source before the advisory

Background

`RenderWidgetHostImpl`
the browser-process object that receives per-frame widget IPC/Mojo messages (via blink::mojom::FrameWidgetHost) from a potentially compromised renderer.
Find-in-page / double-tap zoom
features where the renderer asks the browser to zoom to a rectangle (rect_to_zoom) it computed, which the browser transforms from the sender’s view coordinates into root-view coordinates.
`TransformPointAndRectToRootView`
the RenderWidgetHostViewBase routine that maps a point/rect from a child view’s coordinate space up to the root (top-level) view.
`gfx::Rect::Intersect` / `Intersects` / `Contains`
geometry helpers; Intersect clips a rect in place to the argument, Intersects merely tests for any overlap, and Contains tests point/rect containment.

Root Cause Analysis

Both zoom-to-rect handlers computed the sender’s view_local_bounds but only tested the renderer-supplied rect_to_zoom for overlap (!view_local_bounds.Intersects(rect_to_zoom)), then transformed the full, unclipped rect_to_zoom to root-view coordinates. A compromised renderer could therefore send a rectangle that barely overlaps its own view but extends far beyond it, and the oversized rect would be mapped into the root view, letting the renderer probe/zoom to locations outside its own view bounds. The pre-patch guards were also short-circuited by !view_local_bounds.IsEmpty() && ..., so when the view had empty bounds the checks were skipped entirely and the transform still ran.

The fix clips the rect: it builds clipped_rect_to_zoom(rect_to_zoom), calls clipped_rect_to_zoom.Intersect(view_local_bounds), returns early if the result IsEmpty(), and transforms the clipped rect; the find-in-page handler additionally hard-requires view_local_bounds.Contains(point) before proceeding. This tightens validation and, per the tests, now forwards only the intersection (e.g. an oversized (-1800,-10,3900,3900) becomes (0,0,200,200)) and forwards nothing when bounds are empty.

Key insight
The handlers validated only that the renderer’s rect overlapped the view and then forwarded the whole unclipped rect, so a large rect could reach coordinates outside the sender’s view; the fix intersects the rect with the view bounds (and requires the point be contained) before transforming, so only in-view geometry is ever forwarded.

Attack Path

  1. Compromise a renderer Gain the ability to send arbitrary FrameWidgetHost messages (the assumed compromised-renderer threat model for this browser-process code).
  2. Send an oversized zoom rect Invoke ZoomToFindInPageRectInMainFrame (or AnimateDoubleTapZoomInMainFrame) with a rect_to_zoom that overlaps the frame’s view but extends far outside it, passing the old overlap-only check.
  3. Force transform of out-of-view geometry The browser transforms the full oversized rect to root-view coordinates, causing zoom/scroll targeting to reference positions outside the renderer’s own view bounds.
  4. Observe the effect Use the resulting zoom/scroll behavior to probe or infer content/position outside the sender’s view (a cross-view positional information leak per the advisory’s “improper input validation”; any concrete cross-origin disclosure is (inferred)).

Impact Assessment

A compromised renderer can, per the advisory, exploit improper input validation in the find-in-page/zoom path to have the browser process act on rectangles that reach outside the sender’s own view bounds, enabling it to probe locations beyond its view. This executes in the browser process on renderer-controlled geometry; the concrete gain beyond out-of-view probing (e.g. reading cross-origin layout/position information) is (inferred) and not directly demonstrated by the patch. The advisory does not indicate in-the-wild exploitation.

Changed Functions

FunctionChangeNotes
TEST_F
content/browser/renderer_host/render_widget_host_unittest.cc
modified

Files Changed

  • content/browser/renderer_host/render_widget_host_unittest.cc
  • content/browser/renderer_host/render_widget_host_impl.cc

Audit Directions

  • Overlap test vs. clip
    Flag browser-process handlers that only Intersects/Contains-test renderer-supplied rects and then forward the original rect; the geometry should be clipped with Intersect before use.
  • Empty-bounds short-circuits
    Review guards of the form !bounds.IsEmpty() && <check>; an empty view disables the whole check and can let unvalidated coordinates through.
  • Renderer-to-root coordinate transforms
    Audit every call to TransformPointAndRectToRootView (and similar cross-view transforms) to ensure the input point/rect was constrained to the sender’s own view bounds first.
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
Loading diff…

Regression Test / PoC

shipped with the fix
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
Loading diff…

Original Bug Report

reported by [email protected]

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:

  1. Calculate target coordinates $(P_x, P_y)$ in the main frame.
  2. Construct an unconstrained rect_to_zoom such that it contains the subframe’s own local origin but centers at the targeted coordinates.
  3. Send the Mojo IPC message ZoomToFindInPageRectInMainFrame(rect_to_zoom) to the browser.
  4. Observe whether a layout zoom or scroll animation is triggered (e.g., via IntersectionObserver or 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.

View on issue tracker