Low watch CVSS 6.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

Low
Severity
6.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in SiteIsolation
DescriptionInsufficient validation of untrusted input in SiteIsolation
ComponentSiteIsolation
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.
Tracker496624084
Fix commite5f143f52b12 (chromium/src) +106/-6
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-03-26 (per vendor advisory)
Fix landed2026-04-09 (public git)
Disclosed2026-05-05
Patch gap26 days — fix public in source before the advisory

Background

`RenderWidgetHostImpl`
the browser-process object representing a renderer widget; it receives IPC/Mojo messages from a renderer, including zoom-animation requests.
`ZoomToFindInPageRectInMainFrame` / double-tap zoom path
browser-side handlers that take a renderer-supplied rect (and point) and forward a page-scale zoom animation to the main frame’s view.
`GetViewBounds()` / `gfx::Rect::Intersects`/`Contains`
geometry helpers; view_local_bounds is the view’s own size-rect, against which the requested rect/point can be checked.
`RenderWidgetHostOwnerDelegate`
the owner interface (implemented by RenderViewHostImpl) through which zoom requests are now routed, replacing a direct RenderViewHostImpl call.

Root Cause Analysis

The browser-side zoom handlers forwarded a renderer-supplied rect_to_zoom (and, for double-tap, a point) to the root view’s zoom animation without checking that the geometry had any relationship to the sender frame’s bounds. A compromised/misbehaving renderer could thus request a zoom to an out-of-frame rectangle, violating the invariant that a subframe’s zoom target should intersect its own view.

The fix constructs gfx::Rect view_local_bounds(view_->GetViewBounds().size()) and, when non-empty, returns early unless the rect Intersects the view bounds (and, for the double-tap path, the point is Contains-ed). The check deliberately uses intersect rather than full contains because, as the commit notes, a valid element may legitimately exceed and be scrollable within its frame.

The patch also re-routes the forwarded call through root_rwhi->owner_delegate()->ZoomToFindInPageRect(...) / AnimateDoubleTapZoom(...) with root_rwhi && root_rwhi->owner_delegate() null checks, and promotes these methods to virtual overrides on RenderWidgetHostOwnerDelegate. These are tightening additions (new bounds validation plus null-safety).

Key insight
The handlers trusted renderer-supplied zoom geometry without validating it against the sender frame’s view; the fix rejects rects/points that do not intersect/contain within view_local_bounds before forwarding the zoom.

Attack Path

  1. Compromise or drive a renderer the attacker controls a renderer (potentially an out-of-process subframe) that can send zoom requests to the browser process.
  2. Send an out-of-bounds zoom request invoke the find-in-page zoom or double-tap zoom with a rect_to_zoom (and point) that lies outside the sender view’s GetViewBounds().
  3. Reach the forwarding path pre-patch, the handler transformed the rect/point to the root view and forwarded it to the main-frame zoom animation with no bounds relationship enforced.
  4. Trigger a cross-frame zoom manipulation the main frame animates a page-scale change driven by geometry a subframe should not control; the specific further consequence is not demonstrated by the diff, so any impact beyond an unauthorized zoom/scroll of the main frame is (inferred).

Impact Assessment

A compromised renderer could supply zoom geometry unrelated to its own frame and cause the browser to animate a main-frame zoom/scale it should not be able to influence, a SiteIsolation input-validation gap; the precondition is a renderer able to send these zoom requests. The advisory rates this low severity and classifies it as insufficient validation of untrusted input in SiteIsolation. No in-the-wild exploitation is indicated, and the patch demonstrates only the missing bounds check, not any memory-safety or cross-origin data impact (those would be (inferred)).

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

Audit Directions

  • Unvalidated renderer geometry
    audit browser-side handlers that accept renderer-supplied gfx::Rect/gfx::Point (zoom, scroll, selection, hit-test) for a bounds relationship check against the sender view before use.
  • Intersect-vs-contains intent
    when adding geometry validation, verify the chosen predicate matches the UX (elements may legitimately overflow their frame), as the commit explicitly chose Intersects over Contains.
  • Cross-frame forwarding to root view
    review paths that transform a subframe’s coordinates to the root view and forward to main-frame actions, ensuring the subframe is authorized to drive that action.
From e5f143f52b12c0d9640ff96bda9ec32b93d9d2dd Mon Sep 17 00:00:00 2001
From: Jonathan Ross <jonross@chromium.org>
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 <kenrb@chromium.org>
Commit-Queue: Jonathan Ross <jonross@chromium.org>
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 vm...@google.com

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