Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in SiteIsolation
DescriptionInsufficient validation of untrusted input in SiteIsolation
ComponentSiteIsolation
Bug ClassLogic Error
Tracker496624084
Fix commite5f143f52b12 (chromium/src) +106/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

Changed Functions

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

Files Changed

  • content/browser/renderer_host/render_view_host_impl.h
  • content/browser/renderer_host/render_widget_host_impl.cc
  • content/browser/renderer_host/render_widget_host_owner_delegate.h
  • content/browser/renderer_host/render_widget_host_unittest.cc
From e5f143f52b12c0d9640ff96bda9ec32b93d9d2dd Mon Sep 17 00:00:00 2001
From: Jonathan Ross <[email protected]>
Date: Thu, 09 Apr 2026 06:52:47 -0700
Subject: [PATCH] Update RenderWidgetHost Zoom Bounds

Add bounds validation to
RenderWidgetHostImpl::ZoomToFindInPageRectInMainFrame and
AnimateDoubleTapZoom to ensure the requested zoom rectangle is intersect
the sender frame's bounds.

It is possible that the target is an html element whose bounds exceeds
that of its containing frame. It would be clipped/scrollable for users.
So we do not restrict this solely to a contains check

TEST=RenderWidgetHostTest.*Zoom*Bounds

Bug: 496624084
Change-Id: I0b33105bd45267e8769ada90840e44486c42a1df
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7726682
Reviewed-by: Ken Buchanan <[email protected]>
Commit-Queue: Jonathan Ross <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1612174}
---

diff --git a/content/browser/renderer_host/render_view_host_impl.h b/content/browser/renderer_host/render_view_host_impl.h
index 89fed16c..55277d9 100644
--- a/content/browser/renderer_host/render_view_host_impl.h
+++ b/content/browser/renderer_host/render_view_host_impl.h
@@ -223,10 +223,11 @@
 
   // Tells the renderer process to request a page-scale animation based on the
   // specified point/rect.
-  void AnimateDoubleTapZoom(const gfx::Point& point, const gfx::Rect& rect);
+  void AnimateDoubleTapZoom(const gfx::Point& point,
+                            const gfx::Rect& rect) override;
 
   // Requests a page-scale animation based on the specified rect.
-  void ZoomToFindInPageRect(const gfx::Rect& rect_to_zoom);
+  void ZoomToFindInPageRect(const gfx::Rect& rect_to_zoom) override;
 
   // Tells the renderer view to focus the first (last if reverse is true) node.
   void SetInitialFocus(bool reverse);
diff --git a/content/browser/renderer_host/render_widget_host_impl.cc b/content/browser/renderer_host/render_widget_host_impl.cc
index fe46ebe4..a97558e 100644
--- a/content/browser/renderer_host/render_widget_host_impl.cc
+++ b/content/browser/renderer_host/render_widget_host_impl.cc
@@ -143,6 +143,7 @@
 #include "ui/events/event.h"
 #include "ui/events/keycodes/dom/dom_code.h"
 #include "ui/gfx/color_space.h"
+#include "ui/gfx/geometry/rect.h"
 #include "ui/gfx/geometry/size_conversions.h"
 #include "ui/gfx/geometry/vector2d_conversions.h"
 #include "ui/gfx/geometry/vector2d_f.h"
@@ -3980,6 +3981,13 @@
     return;
   }
 
+  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))) {
+    return;
+  }
+
   auto* root_view = view_->GetRootView();
   gfx::Point transformed_point(point);
   gfx::Rect transformed_rect_to_zoom(rect_to_zoom);
@@ -3989,8 +3997,12 @@
     return;
   }
 
-  auto* root_rvhi = RenderViewHostImpl::From(root_view->GetRenderWidgetHost());
-  root_rvhi->AnimateDoubleTapZoom(transformed_point, transformed_rect_to_zoom);
+  auto* root_rwhi =
+      RenderWidgetHostImpl::From(root_view->GetRenderWidgetHost());
+  if (root_rwhi && root_rwhi->owner_delegate()) {
+    root_rwhi->owner_delegate()->AnimateDoubleTapZoom(transformed_point,
+                                                      transformed_rect_to_zoom);
+  }
 }
 
 void RenderWidgetHostImpl::ZoomToFindInPageRectInMainFrame(
@@ -3999,6 +4011,12 @@
     return;
   }
 
+  gfx::Rect view_local_bounds(view_->GetViewBounds().size());
+  if (!view_local_bounds.IsEmpty() &&
+      !view_local_bounds.Intersects(rect_to_zoom)) {
+    return;
+  }
+
   auto* root_view = view_->GetRootView();
   gfx::Rect transformed_rect_to_zoom(rect_to_zoom);
   if (!TransformPointAndRectToRootView(view_.get(), root_view, nullptr,
@@ -4006,8 +4024,11 @@
     return;
   }
 
-  auto* root_rvhi = RenderViewHostImpl::From(root_view->GetRenderWidgetHost());
-  root_rvhi->ZoomToFindInPageRect(transformed_rect_to_zoom);
+  auto* root_rwhi =
+      RenderWidgetHostImpl::From(root_view->GetRenderWidgetHost());
+  if (root_rwhi && root_rwhi->owner_delegate()) {
+    root_rwhi->owner_delegate()->ZoomToFindInPageRect(transformed_rect_to_zoom);
+  }
 }
 
 void RenderWidgetHostImpl::SetHasTouchEventConsumers(
diff --git a/content/browser/renderer_host/render_widget_host_owner_delegate.h b/content/browser/renderer_host/render_widget_host_owner_delegate.h
index a01d7c14..08078b2 100644
--- a/content/browser/renderer_host/render_widget_host_owner_delegate.h
+++ b/content/browser/renderer_host/render_widget_host_owner_delegate.h
@@ -7,6 +7,8 @@
 
 #include "build/build_config.h"
 #include "third_party/blink/public/common/widget/visual_properties.h"
+#include "ui/gfx/geometry/point.h"
+#include "ui/gfx/geometry/rect.h"
 
 namespace blink {
 namespace web_pref {
@@ -57,6 +59,13 @@
   // between all widgets for the page.
   virtual blink::web_pref::WebPreferences GetWebkitPreferencesForWidget() = 0;
 
+  // Zoom into a specific rect on the page.
+  virtual void ZoomToFindInPageRect(const gfx::Rect& rect_to_zoom) = 0;
+
+  // Animate a double tap zoom to a specific point and rect on the page.
+  virtual void AnimateDoubleTapZoom(const gfx::Point& point,
+                                    const gfx::Rect& rect) = 0;
+
  protected:
   virtual ~RenderWidgetHostOwnerDelegate() {}
 };
diff --git a/content/browser/renderer_host/render_widget_host_unittest.cc b/content/browser/renderer_host/render_widget_host_unittest.cc
index db5e430..bef04fb 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -35,7 +35,9 @@
 #include "content/browser/renderer_host/frame_token_message_queue.h"
 #include "content/browser/renderer_host/input/touch_emulator_impl.h"
 #include "content/browser/renderer_host/mock_render_widget_host.h"
+#include "content/browser/renderer_host/render_view_host_delegate.h"
 #include "content/browser/renderer_host/render_view_host_delegate_view.h"
+#include "content/browser/renderer_host/render_view_host_impl.h"
 #include "content/browser/renderer_host/render_widget_host_delegate.h"
 #include "content/browser/renderer_host/render_widget_host_view_base.h"
 #include "content/browser/renderer_host/visible_time_request_trigger.h"
@@ -555,6 +557,8 @@
  public:
   MOCK_METHOD1(SetBackgroundOpaque, void(bool opaque));
   MOCK_METHOD0(IsMainFrameActive, bool());
+  MOCK_METHOD1(ZoomToFindInPageRect, void(const gfx::Rect&));
+  MOCK_METHOD2(AnimateDoubleTapZoom, void(const gfx::Point&, const gfx::Rect&));
 };
 
 // RenderWidgetHostTest --------------------------------------------------------
@@ -2705,4 +2709,66 @@
   EXPECT_EQ(host_->GetHungRendererDelayForTesting(), base::Seconds(3));
 }
 
+TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectBoundsCheck) {
+  view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+  // Rect outside the view's bounds.
+  gfx::Rect out_of_bounds_rect(-10, -10, 5, 5);
+
+  // With the fix, it should return early because of bounds check.
+  // EXPECT_CALL ensures that ZoomToFindInPageRect is NOT called.
+  EXPECT_CALL(mock_owner_delegate_, ZoomToFindInPageRect(_)).Times(0);
+
+  static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+      ->ZoomToFindInPageRectInMainFrame(out_of_bounds_rect);
+}
+
+TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectValidBounds) {
+  view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+  // Rect inside the view's bounds.
+  gfx::Rect valid_rect(10, 10, 5, 5);
+
+  // This should proceed past the bounds check and call ZoomToFindInPageRect.
+  // The coordinates are relative to the view. Since this is the root view,
+  // they should not be transformed.
+  EXPECT_CALL(mock_owner_delegate_,
+              ZoomToFindInPageRect(gfx::Rect(10, 10, 5, 5)))
+      .Times(1);
+
+  static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+      ->ZoomToFindInPageRectInMainFrame(valid_rect);
+}
+
+TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomBoundsCheck) {
+  view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+  // Rect outside the view's bounds.
+  gfx::Rect out_of_bounds_rect(-10, -10, 5, 5);
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 db5e430..bef04fb 100644
--- a/content/browser/renderer_host/render_widget_host_unittest.cc
+++ b/content/browser/renderer_host/render_widget_host_unittest.cc
@@ -35,7 +35,9 @@
 #include "content/browser/renderer_host/frame_token_message_queue.h"
 #include "content/browser/renderer_host/input/touch_emulator_impl.h"
 #include "content/browser/renderer_host/mock_render_widget_host.h"
+#include "content/browser/renderer_host/render_view_host_delegate.h"
 #include "content/browser/renderer_host/render_view_host_delegate_view.h"
+#include "content/browser/renderer_host/render_view_host_impl.h"
 #include "content/browser/renderer_host/render_widget_host_delegate.h"
 #include "content/browser/renderer_host/render_widget_host_view_base.h"
 #include "content/browser/renderer_host/visible_time_request_trigger.h"
@@ -555,6 +557,8 @@
  public:
   MOCK_METHOD1(SetBackgroundOpaque, void(bool opaque));
   MOCK_METHOD0(IsMainFrameActive, bool());
+  MOCK_METHOD1(ZoomToFindInPageRect, void(const gfx::Rect&));
+  MOCK_METHOD2(AnimateDoubleTapZoom, void(const gfx::Point&, const gfx::Rect&));
 };
 
 // RenderWidgetHostTest --------------------------------------------------------
@@ -2705,4 +2709,66 @@
   EXPECT_EQ(host_->GetHungRendererDelayForTesting(), base::Seconds(3));
 }
 
+TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectBoundsCheck) {
+  view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+  // Rect outside the view's bounds.
+  gfx::Rect out_of_bounds_rect(-10, -10, 5, 5);
+
+  // With the fix, it should return early because of bounds check.
+  // EXPECT_CALL ensures that ZoomToFindInPageRect is NOT called.
+  EXPECT_CALL(mock_owner_delegate_, ZoomToFindInPageRect(_)).Times(0);
+
+  static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+      ->ZoomToFindInPageRectInMainFrame(out_of_bounds_rect);
+}
+
+TEST_F(RenderWidgetHostTest, ZoomToFindInPageRectValidBounds) {
+  view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+  // Rect inside the view's bounds.
+  gfx::Rect valid_rect(10, 10, 5, 5);
+
+  // This should proceed past the bounds check and call ZoomToFindInPageRect.
+  // The coordinates are relative to the view. Since this is the root view,
+  // they should not be transformed.
+  EXPECT_CALL(mock_owner_delegate_,
+              ZoomToFindInPageRect(gfx::Rect(10, 10, 5, 5)))
+      .Times(1);
+
+  static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+      ->ZoomToFindInPageRectInMainFrame(valid_rect);
+}
+
+TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomBoundsCheck) {
+  view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+  // Rect outside the view's bounds.
+  gfx::Rect out_of_bounds_rect(-10, -10, 5, 5);
+  gfx::Point tap_point(10, 10);
+
+  // With the fix, it should return early because of bounds check.
+  // EXPECT_CALL ensures that AnimateDoubleTapZoom is NOT called.
+  EXPECT_CALL(mock_owner_delegate_, AnimateDoubleTapZoom(_, _)).Times(0);
+
+  static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+      ->AnimateDoubleTapZoomInMainFrame(tap_point, out_of_bounds_rect);
+}
+
+TEST_F(RenderWidgetHostTest, AnimateDoubleTapZoomValidBounds) {
+  view_->SetBounds(gfx::Rect(0, 0, 200, 200));
+
+  // Rect inside the view's bounds.
+  gfx::Rect valid_rect(10, 10, 5, 5);
+  gfx::Point tap_point(12, 12);
+
+  // This should proceed past the bounds check and call AnimateDoubleTapZoom.
+  EXPECT_CALL(mock_owner_delegate_,
+              AnimateDoubleTapZoom(gfx::Point(12, 12), gfx::Rect(10, 10, 5, 5)))
+      .Times(1);
+
+  static_cast<blink::mojom::FrameWidgetHost*>(host_.get())
+      ->AnimateDoubleTapZoomInMainFrame(tap_point, valid_rect);
+}
+
 }  // namespace content
diff --git a/content/test/stub_render_widget_host_owner_delegate.h b/content/test/stub_render_widget_host_owner_delegate.h
index 9d230e6..52c6daed 100644
--- a/content/test/stub_render_widget_host_owner_delegate.h
+++ b/content/test/stub_render_widget_host_owner_delegate.h
@@ -20,6 +20,9 @@
   void SetBackgroundOpaque(bool opaque) override {}
   bool IsMainFrameActive() override;
   blink::web_pref::WebPreferences GetWebkitPreferencesForWidget() override;
+  void ZoomToFindInPageRect(const gfx::Rect& rect_to_zoom) override {}
+  void AnimateDoubleTapZoom(const gfx::Point& point,
+                            const gfx::Rect& rect) override {}
 };
 
 }  // namespace content
Loading diff…

Original Bug Report

reported by [email protected]

Site Isolation Bypass: Cross-Origin Hit-Test Oracle via ZoomToFindInPageRectInMainFrame

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised Out-of-Process iframe (OOPIF) can bypass Site Isolation by sending a ZoomToFindInPageRectInMainFrame Mojo IPC to the browser process with arbitrary coordinates. The browser forwards this request to the main frame’s renderer without bounds checking, triggering an unconstrained hit-test that ignores viewport clipping. By observing the resulting page scale and scroll offset changes, the attacker can infer the size, position, and presence of cross-origin elements in the main frame.

Affected files:

  • content/browser/renderer_host/render_widget_host_impl.cc
  • third_party/blink/renderer/core/exported/web_view_impl.cc
  • third_party/blink/renderer/core/frame/web_frame_widget_impl.cc
  • content/browser/renderer_host/render_view_host_impl.cc

Estimated timestamp from git blame: 2025-06-04

Summary

A compromised Out-of-Process iframe (OOPIF) can send a ZoomToFindInPageRectInMainFrame Mojo IPC message to the browser process, specifying arbitrary coordinates within the main frame. The browser process forwards this request to the main frame’s renderer without verifying that the coordinates fall within the subframe’s visible bounds. The main frame renderer then performs an unconstrained hit-test (ignoring viewport clipping) at the target coordinates. If an element is hit, the renderer calculates a new page scale factor based on the element’s width and a new scroll offset based on its position, triggering a zoom animation.

Because these new visual properties are broadcasted back to all frames (including the attacker’s OOPIF), the attacker can observe the resulting page scale (window.visualViewport.scale) and deduce the exact width of the cross-origin element. By systematically probing different coordinates, the attacker can map the layout of the main frame’s DOM, bypassing Site Isolation protections and potentially leaking sensitive user data or application state.

Potential Attacker Steps

Note: These steps describe a potential attack path. A full working Proof of Concept has not yet been developed or executed by this agent.

  1. Attacker Compromise: The attacker compromises an Out-of-Process iframe (OOPIF) running in a separate renderer process, gaining the ability to execute arbitrary code within that renderer.
  2. Crafting the Payload: The compromised OOPIF renderer crafts a malicious gfx::Rect representing target coordinates it wishes to probe within the main frame’s document. The coordinates are chosen relative to the iframe’s local coordinate space.
  3. Sending the IPC: The OOPIF renderer sends a FrameWidgetHost::ZoomToFindInPageRectInMainFrame Mojo IPC message to the browser process, passing the crafted gfx::Rect. This interface is accessible to subframes and is defined in third_party/blink/public/mojom/page/widget.mojom:211.
  4. Browser Processing: The browser process receives this IPC in RenderWidgetHostImpl::ZoomToFindInPageRectInMainFrame (content/browser/renderer_host/render_widget_host_impl.cc:3956).
  5. Missing Bounds Check: TransformPointAndRectToRootView (content/browser/renderer_host/render_widget_host_impl.cc:3913) translates the coordinates but fails to clip the transformed rect to the subframe’s actual visible boundaries.
  6. Forwarding to Main Frame: The browser forwards the unclipped rectangle to the main frame’s RenderViewHostImpl, which sends a ZoomToFindInPageRect Mojo message to the main frame’s renderer process.
  7. Initiating Hit Test: WebViewImpl::ZoomToFindInPageRect (third_party/blink/renderer/core/exported/web_view_impl.cc:1009) calls MainFrameImpl()->FrameWidgetImpl()->ComputeBlockBound(...) with ignore_clipping=true.
  8. Unconstrained DOM Traversal: The renderer performs a hit test using HitTestResultAtLocation. Due to kIgnoreClipping, the hit test ignores the main frame’s viewport clipping boundaries, reaching off-screen elements.
  9. Presence Oracle: If no element is hit, the function returns early (third_party/blink/renderer/core/exported/web_view_impl.cc:1018). This provides a binary oracle: side effects only occur if an element exists at the target coordinates.
  10. Width & Position Oracle: If an element is hit, ComputeScaleAndScrollForBlockRect calculates a new page scale factor based on the hit block’s width (third_party/blink/renderer/core/exported/web_view_impl.cc:819) and a new scroll offset (third_party/blink/renderer/core/exported/web_view_impl.cc:854).
  11. Applying Side Effects: WebViewImpl::ZoomToFindInPageRect applies these values via StartPageScaleAnimation (third_party/blink/renderer/core/exported/web_view_impl.cc:1031).
  12. Observing the Leak: The new page scale factor and scroll offset are synchronized to the browser and broadcasted to all frames via VisualProperties updates. The attacker’s OOPIF observes the scale directly (e.g., window.visualViewport.scale), mathematically deriving the exact width of the cross-origin element. Iterative probing allows systematic mapping of the main frame’s layout.

Suggested Fix

  1. Browser-Side Validation: RenderWidgetHostImpl::ZoomToFindInPageRectInMainFrame must validate that the requested rect_to_zoom falls entirely within the bounds of the sender’s subframe (e.g., by intersecting it with the subframe’s known visible rect). Requests specifying coordinates outside the subframe should be dropped or treated as a bad message.
  2. Verify Find-in-Page State: The browser should only accept ZoomToFindInPageRectInMainFrame IPCs if a legitimate Find-in-Page session is active and the requesting frame actually contains the highlighted find result. A subframe should not be able to trigger this zoom animation arbitrarily at any time.

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


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. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker