Medium watch CVSS 5.4 chrome Spoofing 🔧 Commit mapped

Overview

Medium
Severity
5.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUI misrepresentation in Geometry
DescriptionUI misrepresentation in Geometry
ComponentGeometry
Sub-componentIOv2
Bug ClassSpoofing
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.
Tracker518035396
Fix commitec8e14090e80 (chromium/src) +262/-4
CWECWE-451
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:L
CISA KEVNot listed
CreditedGoogle
Reported2026-05-29 (per vendor advisory)
Fix landed2026-07-10 (public git)
Disclosed2026-08-25
Patch gap46 days — fix public in source before the advisory

Background

IntersectionObserver v2 (visibility)
With trackVisibility set, the observer reports not just whether a target intersects the viewport but whether it is actually visible, i.e. not occluded by other painted content.
Occlusion hit test
Blink determines occlusion by hit testing with HitTestRequest::IsHitTestVisualOverflow(), walking painted content (including ink outside layout bounds, such as drop-shadow) that could cover the target.
Table parts
Table structural fragments (IsTableRow, IsTableSection, and other IsTablePart) that are not normally themselves hit targets but can still emit ink (e.g. via filter: drop-shadow).
`IsVisibleToPaint` / `IsVisibleToHitTest`
Helpers deciding whether a fragment paints and whether its style/request make it a hit target; occlusion detection depends on both.

Root Cause Analysis

The free function IsVisibleToHitTest(const PhysicalFragment&, const HitTestRequest&) in box_fragment_painter.cc previously returned IsVisibleToPaint(fragment, style) && IsVisibleToHitTest(style, request), so a fragment that was not visible-to-paint was never treated as a hit target even during occlusion testing. Separately, the self-hit-testing logic (a method whose name is not in the hunk header) unconditionally set hit_test_self = false for IsTableRow()/IsTableSection(), since table rows and sections are never regular hit targets. The consequence is that a table part that is not itself visible (e.g. visibility: hidden) but casts ink over a target via drop-shadow was skipped during occlusion hit testing, so IntersectionObserver v2 reported the target as visible when it was actually occluded.

The fix relaxes both paths specifically for occlusion: IsVisibleToHitTest now returns true for request.IsHitTestVisualOverflow() && fragment.IsTablePart() even when not visible-to-paint, and the self-hit-test sets hit_test_self = hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow() for rows/sections. This lets table parts contribute to occlusion so visibility is reported correctly.

Key insight
Occlusion hit testing excluded table parts that were not themselves paint-visible, so their ink (e.g. drop-shadow) could hide a target while IntersectionObserver v2 still reported it visible; the fix makes table parts (and rows/sections) hit targets specifically under IsHitTestVisualOverflow().

Attack Path

  1. Set up a tracked target A page observes a target element with an IntersectionObserver v2 (trackVisibility: true).
  2. Overlay with an invisible table part The page positions a table whose part (row, cell, section, or the table itself) is visibility: hidden yet uses filter: drop-shadow(...) to cast ink over the target (as in the TableRowOcclusion/TableOcclusion tests).
  3. Evade occlusion detection Because the invisible table part was skipped by occlusion hit testing, the observer reported the target as visible despite being covered by the drop-shadow.
  4. Misrepresent visibility Content or UI that relies on the visibility signal (e.g. gating an action on an element being genuinely visible) is deceived, the UI-misrepresentation/spoofing impact the advisory describes.

Impact Assessment

A web page can occlude a visibility-tracked element with the ink of an otherwise-invisible table part and have IntersectionObserver v2 still report it as visible, a spoofing/UI-misrepresentation defect in the renderer as the advisory states. The precondition is a target tracked with trackVisibility and an attacker-controlled table element casting ink over it. The advisory does not report in-the-wild exploitation, and use of this to defeat any specific visibility-gated security control is (inferred).

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/paint/box_fragment_painter.cc
modified
TEST_F
third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
modified

Files Changed

  • third_party/blink/renderer/core/paint/box_fragment_painter.cc
  • third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc

Audit Directions

  • Occlusion vs regular hit testing divergence
    Review fragment types excluded from hit testing (table parts, SVG, pseudo-content) and confirm each still contributes to IsHitTestVisualOverflow occlusion where it can emit ink.
  • Ink beyond layout bounds
    Audit visibility/occlusion logic for effects that paint outside a box (drop-shadow, box-shadow, outline, overflowing filters) on elements that are not themselves paint-visible.
  • Visibility-signal trust
    Look for features that gate behavior on IntersectionObserver v2 visibility and consider whether occlusion coverage is complete enough to rely on for security decisions.
From ec8e14090e80ee238dbc34ef907967f879c3662d Mon Sep 17 00:00:00 2001
From: Stefan Zager <[email protected]>
Date: Fri, 10 Jul 2026 16:29:04 -0700
Subject: [PATCH] [IOv2] Hit test table parts and rows for occlusion

Bug: 518035396
Change-Id: Ibfd1df5ce1e62c4d0de73728215348bd4cb8c339
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8068547
Reviewed-by: Philip Rogers <[email protected]>
Commit-Queue: Stefan Zager <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1660595}
---
diff --git a/third_party/blink/renderer/core/paint/box_fragment_painter.cc b/third_party/blink/renderer/core/paint/box_fragment_painter.cc
index f90aa3b..e332e208 100644
--- a/third_party/blink/renderer/core/paint/box_fragment_painter.cc
+++ b/third_party/blink/renderer/core/paint/box_fragment_painter.cc
@@ -168,8 +168,19 @@
 inline bool IsVisibleToHitTest(const PhysicalFragment& fragment,
                                const HitTestRequest& request) {
   const ComputedStyle& style = fragment.Style();
-  return IsVisibleToPaint(fragment, style) &&
-         IsVisibleToHitTest(style, request);
+  if (!IsVisibleToHitTest(style, request)) {
+    return false;
+  }
+  if (IsVisibleToPaint(fragment, style)) {
+    return true;
+  }
+  // Table parts that are not themselves visible can still contribute ink
+  // to visible descendants.
+  if (request.IsHitTestVisualOverflow() && fragment.IsTablePart())
+      [[unlikely]] {
+    return true;
+  }
+  return false;
 }
 
 // Hit tests inline ancestor elements of |fragment| who do not have their own
@@ -2489,11 +2500,13 @@
   bool pointer_events_bounding_box = false;
   bool hit_test_self = fragment.IsInSelfHitTestingPhase(hit_test.phase);
   if (hit_test_self) {
-    // Table row and table section are never a hit target.
+    // Table row and table section are never a hit target for regular hit test,
+    // but they are targets when testing for occlusion.
     // SVG <text> is not a hit target except if 'pointer-events: bounding-box'.
     if (GetPhysicalFragment().IsTableRow() ||
         GetPhysicalFragment().IsTableSection()) {
-      hit_test_self = false;
+      hit_test_self =
+          hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow();
     } else if (fragment.IsSvgText()) {
       pointer_events_bounding_box =
           fragment.Style().UsedPointerEvents() == EPointerEvents::kBoundingBox;
diff --git a/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc b/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
index 3f8e018..f0f73f2e 100644
--- a/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
+++ b/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
@@ -2810,6 +2810,251 @@
   EXPECT_TRUE(observer_delegate->LastEntry()->isVisible());
 }
 
+TEST_F(IntersectionObserverV2Test, TableRowOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        visibility: hidden;
+        border-collapse: collapse;
+      }
+      #row {
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #cell {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <tr id='row'>
+        <td id='cell'></td>
+      </tr>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table row.
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
+TEST_F(IntersectionObserverV2Test, TableCellOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        border-collapse: collapse;
+      }
+      #cell {
+        visibility: hidden;
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #child {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <tr>
+        <td id='cell'><div id='child'></div></td>
+      </tr>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table cell.
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
+TEST_F(IntersectionObserverV2Test, TableHeaderGroupOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        visibility: hidden;
+        border-collapse: collapse;
+      }
+      #header {
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #cell {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <thead id='header'>
+        <tr>
+          <td id='cell'></td>
+        </tr>
+      </thead>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table header group
+  // (thead).
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
+TEST_F(IntersectionObserverV2Test, TableOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        visibility: hidden;
+        border-collapse: collapse;
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #cell {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <tr>
+        <td id='cell'></td>
+      </tr>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table.
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
 TEST_F(IntersectionObserverV2Test, BasicOpacity) {
   WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
   SimRequest main_resource("https://example.com/", "text/html");
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc b/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
index 3f8e018..f0f73f2e 100644
--- a/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
+++ b/third_party/blink/renderer/core/intersection_observer/intersection_observer_test.cc
@@ -2810,6 +2810,251 @@
   EXPECT_TRUE(observer_delegate->LastEntry()->isVisible());
 }
 
+TEST_F(IntersectionObserverV2Test, TableRowOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        visibility: hidden;
+        border-collapse: collapse;
+      }
+      #row {
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #cell {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <tr id='row'>
+        <td id='cell'></td>
+      </tr>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table row.
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
+TEST_F(IntersectionObserverV2Test, TableCellOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        border-collapse: collapse;
+      }
+      #cell {
+        visibility: hidden;
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #child {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <tr>
+        <td id='cell'><div id='child'></div></td>
+      </tr>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table cell.
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
+TEST_F(IntersectionObserverV2Test, TableHeaderGroupOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        visibility: hidden;
+        border-collapse: collapse;
+      }
+      #header {
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #cell {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <thead id='header'>
+        <tr>
+          <td id='cell'></td>
+        </tr>
+      </thead>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table header group
+  // (thead).
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
+TEST_F(IntersectionObserverV2Test, TableOcclusion) {
+  WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
+  SimRequest main_resource("https://example.com/", "text/html");
+  LoadURL("https://example.com/");
+  main_resource.Complete(R"HTML(
+    <style>
+      #target {
+        width: 100px;
+        height: 100px;
+      }
+      #table {
+        position: absolute;
+        left: 200px;
+        top: 0;
+        visibility: hidden;
+        border-collapse: collapse;
+        filter: drop-shadow(-200px 0 0 black);
+      }
+      #cell {
+        visibility: visible;
+        width: 100px;
+        height: 100px;
+        background: black;
+      }
+    </style>
+    <div id='target'></div>
+    <table id='table'>
+      <tr>
+        <td id='cell'></td>
+      </tr>
+    </table>
+  )HTML");
+  Compositor().BeginFrame();
+
+  IntersectionObserverInit* observer_init = IntersectionObserverInit::Create();
+  observer_init->setTrackVisibility(true);
+  observer_init->setDelay(100);
+  DummyExceptionStateForTesting exception_state;
+  TestIntersectionObserverDelegate* observer_delegate =
+      MakeGarbageCollected<TestIntersectionObserverDelegate>(GetDocument());
+  IntersectionObserver* observer = IntersectionObserver::Create(
+      observer_init, *observer_delegate,
+      LocalFrameUkmAggregator::kJavascriptIntersectionObserver,
+      exception_state);
+  ASSERT_FALSE(exception_state.HadException());
+  Element* target = GetDocument().getElementById(AtomicString("target"));
+  ASSERT_TRUE(target);
+  observer->observe(target);
+
+  Compositor().BeginFrame();
+  test::RunPendingTasks();
+  ASSERT_FALSE(Compositor().NeedsBeginFrame());
+  EXPECT_EQ(observer_delegate->CallCount(), 1);
+  EXPECT_EQ(observer_delegate->EntryCount(), 1);
+  EXPECT_TRUE(observer_delegate->LastEntry()->isIntersecting());
+  // The target is occluded by the drop-shadow of the table.
+  EXPECT_FALSE(observer_delegate->LastEntry()->isVisible());
+}
+
 TEST_F(IntersectionObserverV2Test, BasicOpacity) {
   WebView().MainFrameViewWidget()->Resize(gfx::Size(800, 600));
   SimRequest main_resource("https://example.com/", "text/html");
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.