Medium watch CVSS 4.3 chrome Policy Bypass 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in Blink
DescriptionPolicy bypass in Blink
ComponentBlink
Bug ClassPolicy Bypass
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.
Tracker430198264
Fix commitcd1b8be76004 (chromium/src) +38/-1
CWECWE-639
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:L/A:N
CISA KEVNot listed
CreditedLyra Rebane (rebane2001)
Reported2025-07-08 · bounty $4000 (per vendor advisory)
Fix landed2026-02-20 (public git)
Disclosed2026-04-07
Patch gap46 days — fix public in source before the advisory

Background

`ReferenceFilterOperation`
the style-level representation of a CSS filter: url(#id) reference filter, created during style recalc and later annotated during pre-paint with its fully resolved compositor filter in filter_.
`IsEqualAssumingSameType`
the per-FilterOperation equality hook used when diffing old vs. new computed style to decide whether filters changed and whether a paint-property update is needed.
Paint-property update / pre-paint annotation
the pre-paint phase that builds compositor filters and writes the resolved filter back onto the style-created ReferenceFilterOperation; it is skipped if the style diff doesn’t flag the element for update.
Visual-overflow hit testing
hit testing against an element’s filter-expanded visual overflow, which relies on the style-created ReferenceFilterOperation carrying the resolved compositor filter.

Root Cause Analysis

During style recalc a ReferenceFilterOperation can be re-created even when the filter is logically unchanged, producing a new object whose filter_ annotation is nullptr (not yet resolved by pre-paint). The pre-patch ReferenceFilterOperation::IsEqualAssumingSameType compared only url_ and resource_, so it reported the re-created operation as equal to the previous one; the style diff then did not flag filters as changed, and if nothing else marked the element for a paint-property update, pre-paint skipped building compositor filters and never annotated the new operation. Rendering still looked correct because the EffectPaintPropertyNode kept pointing at the previously computed compositor filter, but the style-created operation was left with a nullptr filter_, which broke hit testing of the filter-expanded visual overflow.

The fix tightens the equality check by also comparing filter_.Get() == other.filter_.Get(): a freshly re-created operation (with filter_ == nullptr) no longer compares equal to the annotated old one, which forces the paint-property update and re-annotation. A pointer comparison suffices because the newly created operation always starts with filter_ == nullptr.

Key insight
The equality operator omitted the resolved compositor-filter annotation, so a re-created-but-unchanged reference filter was treated as identical and skipped re-annotation; adding filter_.Get() to the comparison makes the stale/unannotated operation compare unequal and forces the paint-property update that re-resolves it.

Attack Path

  1. Apply a reference filter Page CSS sets filter: url(#displace) on a positioned element whose visual overflow is expanded by the SVG filter (the shipped test uses feDisplacementMap scale="250", overflow rect -10,-10,120,120).
  2. Force a benign style recalc Author script mutates an unrelated property (the test sets opacity: 1) that triggers style recalc and re-creation of the ReferenceFilterOperation without otherwise marking the element for paint-property update.
  3. Equality masks the change The pre-patch IsEqualAssumingSameType returns equal (only url_/resource_ match), so filters are not flagged as changed and pre-paint skips re-annotating the new operation, leaving filter_ == nullptr.
  4. Hit testing diverges Hit testing of the filter-expanded visual overflow now uses the unannotated operation and yields incorrect results, the input-targeting/policy inconsistency the advisory classifies as a policy bypass.

Impact Assessment

An attacker-controlled page can drive a benign style recalc that leaves a reference filter’s visual-overflow hit-test region stale, causing hit testing to disagree with the rendered output; the vendor advisory classifies this as a policy bypass in Blink with medium severity. It is reachable by ordinary web content in a stock build (CSS reference filter plus a scripted style change), runs in the renderer, and the diff shows a hit-testing/visual-overflow inconsistency rather than any memory-safety primitive, so no escalation beyond the described behavior is demonstrated.

Changed Functions

FunctionChangeNotes
TEST_F
third_party/blink/renderer/core/layout/hit_testing_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/layout/hit_testing_test.cc
  • third_party/blink/renderer/core/style/filter_operation.cc

Audit Directions

  • Incomplete equality operators
    Audit FilterOperation/style-object equality hooks for fields (resolved/cached annotations, pointers populated in a later phase) that affect behavior but are excluded from the comparison that gates invalidation.
  • Phase-populated state vs. style diff
    Look for data written during pre-paint/compositing that the style-recalc equality check does not account for, where a re-created-but-equal object silently loses that annotation.
  • Render-correct but hit-test-wrong paths
    Review features where the compositor path and the hit-test path read from different objects (EffectPaintPropertyNode vs. style-created operation); divergence there can produce input-targeting mismatches without visible rendering bugs.
From cd1b8be760040aeaa4ad11783bdcf550947920bd Mon Sep 17 00:00:00 2001
From: Stefan Zager <szager@chromium.org>
Date: Fri, 20 Feb 2026 13:22:41 -0800
Subject: [PATCH] Fix equality operator for blink::ReferenceFilterOp

Filter operations are created during style recalc, but some of them
(specifically, box-reflect and reference filters) need layout
information to compute their visual overflow extent. For box-reflect,
this is handled by always creating the filter operation ad hoc when
requested[1], presumably after pre-paint has generated the necessary
geometry information. For reference filters, this is handled by
annotating the style-generated ReferenceFilterOp with its fully resolved
compositor filter during pre-paint[2].

It can happen that a ReferenceFilterOp is re-created during style
recalc, even when the operation has not changed. When this happens, the
style diff passed to StyleDidChange() won't indicate that filters have
changed, because the equality comparison for ReferenceFilterOp doesn't
check whether the resolved compositor filters match. If nothing else in
the style diff causes the element to be marked for paint property
update, it will skip the building of compositor filters during
pre-paint, and the ReferenceFilterOp will not get annotated with its
resolved compositor filter. This doesn't break rendering, because the
EffectPaintPropertyNode will continue to point to the
previously-computed compositor filter. However, it *does* break hit
testing of visual overflow, which relies on the style-created
FilterReferenceOp being annotated with the compositor filter[3].

This CL changes the equality comparison for ReferenceFilterOp so that it
considers the annotated compositor filter. A pointer comparison is
sufficient; for a newly-created ReferenceFilterOp it will always be
`nullptr` and should force paint property update.

[1]https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/paint/paint_layer.cc;drc=d209eefee6037bd0905e43f88570fda8edab89a1;l=2397

[2]https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/paint/filter_effect_builder.cc;drc=3bbce24997c008ef45d4253542ec7f0a5a97e571;l=402

[3]https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/style/filter_operation.cc;drc=d209eefee6037bd0905e43f88570fda8edab89a1;l=46

Bug: 430198264
Change-Id: I6f3215a603a6ed831431635c37d865216d0bb333
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7597074
Commit-Queue: Stefan Zager <szager@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1588040}
---

diff --git a/third_party/blink/renderer/core/layout/hit_testing_test.cc b/third_party/blink/renderer/core/layout/hit_testing_test.cc
index 1699283..36e44fb 100644
--- a/third_party/blink/renderer/core/layout/hit_testing_test.cc
+++ b/third_party/blink/renderer/core/layout/hit_testing_test.cc
@@ -274,4 +274,40 @@
             HitTest(PhysicalOffset(5, 5)));
 }
 
+TEST_F(HitTestingTest, ReferenceFilter) {
+  SetBodyInnerHTML(R"HTML(
+<style>
+  #target {
+    position:absolute;
+    top:100px;
+    left:100px;
+    width:100px;
+    height:100px;
+    background-color:blue;
+    filter:url(#displace);
+  }
+</style>
+<div id="target"></div>
+<svg width="100" height="100" viewBox="0 0 100 100">
+  <filter id="displace">
+      <feFlood />
+      <feDisplacementMap
+        scale="250"
+        xChannelSelector="R"
+        yChannelSelector="G" />
+  </filter>
+</svg>
+  )HTML");
+
+  Element* target = GetElementById("target");
+  LayoutBox* box = To<LayoutBox>(target->GetLayoutObject());
+  EXPECT_EQ(box->VisualOverflowRectIncludingFilters(),
+            PhysicalRect(-10, -10, 120, 120));
+
+  target->SetInlineStyleProperty(CSSPropertyID::kOpacity, "1");
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_EQ(box->VisualOverflowRectIncludingFilters(),
+            PhysicalRect(-10, -10, 120, 120));
+}
+
 }  // namespace blink
diff --git a/third_party/blink/renderer/core/style/filter_operation.cc b/third_party/blink/renderer/core/style/filter_operation.cc
index 3bfbf65c..b8a312e 100644
--- a/third_party/blink/renderer/core/style/filter_operation.cc
+++ b/third_party/blink/renderer/core/style/filter_operation.cc
@@ -71,7 +71,8 @@
 bool ReferenceFilterOperation::IsEqualAssumingSameType(
     const FilterOperation& o) const {
   const auto& other = To<ReferenceFilterOperation>(o);
-  return url_ == other.url_ && resource_ == other.resource_;
+  return url_ == other.url_ && resource_ == other.resource_ &&
+         filter_.Get() == other.filter_.Get();
 }
 
 gfx::RectF BlurFilterOperation::MapRect(const gfx::RectF& rect) const {
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/layout/hit_testing_test.cc b/third_party/blink/renderer/core/layout/hit_testing_test.cc
index 1699283..36e44fb 100644
--- a/third_party/blink/renderer/core/layout/hit_testing_test.cc
+++ b/third_party/blink/renderer/core/layout/hit_testing_test.cc
@@ -274,4 +274,40 @@
             HitTest(PhysicalOffset(5, 5)));
 }
 
+TEST_F(HitTestingTest, ReferenceFilter) {
+  SetBodyInnerHTML(R"HTML(
+<style>
+  #target {
+    position:absolute;
+    top:100px;
+    left:100px;
+    width:100px;
+    height:100px;
+    background-color:blue;
+    filter:url(#displace);
+  }
+</style>
+<div id="target"></div>
+<svg width="100" height="100" viewBox="0 0 100 100">
+  <filter id="displace">
+      <feFlood />
+      <feDisplacementMap
+        scale="250"
+        xChannelSelector="R"
+        yChannelSelector="G" />
+  </filter>
+</svg>
+  )HTML");
+
+  Element* target = GetElementById("target");
+  LayoutBox* box = To<LayoutBox>(target->GetLayoutObject());
+  EXPECT_EQ(box->VisualOverflowRectIncludingFilters(),
+            PhysicalRect(-10, -10, 120, 120));
+
+  target->SetInlineStyleProperty(CSSPropertyID::kOpacity, "1");
+  UpdateAllLifecyclePhasesForTest();
+  EXPECT_EQ(box->VisualOverflowRectIncludingFilters(),
+            PhysicalRect(-10, -10, 120, 120));
+}
+
 }  // namespace blink
Loading diff…

Original Bug Report

reported by re...@gmail.com

IntersectionObserver's visibility check can be bypassed through SVG filters

VULNERABILITY DETAILS
IntersectionObserver has the trackVisibility option to make sure that an element is visible to the user. This is used to prevent clickjacking, since a parent frame can no longer cover the element with its own graphics in order to trick the user into performing an unwanted action.

However, through some trickery, an undetectable overlay can still be created by using SVG filters. For example, it is possible to use the feDisplacementMap filter to put graphics in an area up to 10% beyond the element bounds.

Doing this would still be detected by the IntersectionObserver, but this can be bypassed by quickly changing the opacity of the element (doing so probably resets its bounds somehow?).

The result is the ability to controllably cover up an iframe with any kinds of graphics without setting off its IntersectionObserver.

VERSION
Chrome Version: Stable, 140.0.7259.2 Dev Operating System: Windows, Android, macOS

REPRODUCTION CASE

  1. Download the included files.
  2. Open invisible-bypass.html.
  3. Observe how the frame on the page gets covered with a fake button, but its background is still green (it’d turn red if not visible).
  4. Click the fake button.

Note: To make testing the repro easier, the target frame is same-origin. This vulnerability works cross-origin too, as demonstrated in my demo video.

CREDIT INFORMATION
Externally reported security bugs may appear in Chrome release notes. If this bug is included, how would you like to be credited?
Reporter credit: Lyra Rebane (rebane2001)

View on issue tracker