Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Paint
DescriptionInappropriate implementation in Paint
ComponentPaint
Bug ClassLogic Error
Tracker514040614
Fix commit820d6d4c0c5b (chromium/src) +90/-4
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • third_party/blink/renderer/core/layout/layout_replaced.cc
  • third_party/blink/renderer/core/paint/box_fragment_painter.cc
  • third_party/blink/renderer/core/paint/paint_layer.cc
  • third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion-replaced.html
  • third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion.html
From 820d6d4c0c5b969ae7559ac69c404380502507d3 Mon Sep 17 00:00:00 2001
From: Philip Rogers <[email protected]>
Date: Tue, 19 May 2026 09:29:14 -0700
Subject: [PATCH] Fix border-radius hit testing for visual overflow

When hit testing visual overflow, we cannot stop hit testing once a hit
is outside the border because effects like box-shadow can still be
present.

Fixed: 514040614
Change-Id: I4c0daacbb659966a4088277a1ef9f8ab5e75f5fb
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7858611
Commit-Queue: Philip Rogers <[email protected]>
Reviewed-by: Stefan Zager <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1632916}
---

diff --git a/third_party/blink/renderer/core/layout/layout_replaced.cc b/third_party/blink/renderer/core/layout/layout_replaced.cc
index b94e2aec..a3de7f17 100644
--- a/third_party/blink/renderer/core/layout/layout_replaced.cc
+++ b/third_party/blink/renderer/core/layout/layout_replaced.cc
@@ -174,7 +174,9 @@
 
   if (StyleRef().HasBorderRadius() &&
       HitTestClippedOutByBorder(hit_test_location, accumulated_offset)) {
-    return false;
+    if (!result.GetHitTestRequest().IsHitTestVisualOverflow()) {
+      return false;
+    }
   }
 
   // Now hit test ourselves.
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 517dfad7..f8e3d31 100644
--- a/third_party/blink/renderer/core/paint/box_fragment_painter.cc
+++ b/third_party/blink/renderer/core/paint/box_fragment_painter.cc
@@ -2419,7 +2419,9 @@
     }
   } else if (style.HasBorderRadius() &&
              HitTestClippedOutByBorder(hit_test.location, physical_offset)) {
-    return false;
+    if (!hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow()) {
+      return false;
+    }
   }
 
   bool pointer_events_bounding_box = false;
diff --git a/third_party/blink/renderer/core/paint/paint_layer.cc b/third_party/blink/renderer/core/paint/paint_layer.cc
index d867675..7e05284 100644
--- a/third_party/blink/renderer/core/paint/paint_layer.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer.cc
@@ -1704,7 +1704,9 @@
         bounds.HasRadius() &&
         HitTestClippedOutByBorderRadius(transform_container, container_fragment,
                                         hit_test_location, bounds)) {
-      continue;
+      if (!result.GetHitTestRequest().IsHitTestVisualOverflow()) {
+        continue;
+      }
     }
 
     inside_clip_rect = true;
@@ -1763,7 +1765,9 @@
         HitTestClippedOutByBorderRadius(transform_container, container_fragment,
                                         recursion_data.location,
                                         fragment.background_rect)) {
-      continue;
+      if (!result.GetHitTestRequest().IsHitTestVisualOverflow()) {
+        continue;
+      }
     }
 
     PaintLayer* hit_layer = HitTestLayerByApplyingTransform(
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion-replaced.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion-replaced.html
new file mode 100644
index 0000000..e9432b2b
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion-replaced.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+  body {
+    margin: 0;
+  }
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: red;
+  }
+  #occluder {
+    position: absolute;
+    left: -20px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    border-radius: 1px;
+    box-shadow: 120px 0px 0px black;
+    background: blue;
+  }
+</style>
+<div id="target"></div>
+<canvas id="occluder"></canvas>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    // The target should be occluded by the box-shadow of the occluder.
+    assert_false(entries[0].isVisible);
+    done();
+  }, {trackVisibility: true, delay: 100}).observe(target);
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion.html
new file mode 100644
index 0000000..659a270d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+  body {
+    margin: 0;
+  }
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: red;
+  }
+  #occluder {
+    position: absolute;
+    left: -20px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    border-radius: 1px;
+    box-shadow: 120px 0px 0px black;
+    background: blue;
+  }
+</style>
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    // The target should be occluded by the box-shadow of the occluder.
+    assert_false(entries[0].isVisible);
+    done();
+  }, {trackVisibility: true, delay: 100}).observe(target);
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion-replaced.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion-replaced.html
new file mode 100644
index 0000000..e9432b2b
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion-replaced.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+  body {
+    margin: 0;
+  }
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: red;
+  }
+  #occluder {
+    position: absolute;
+    left: -20px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    border-radius: 1px;
+    box-shadow: 120px 0px 0px black;
+    background: blue;
+  }
+</style>
+<div id="target"></div>
+<canvas id="occluder"></canvas>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    // The target should be occluded by the box-shadow of the occluder.
+    assert_false(entries[0].isVisible);
+    done();
+  }, {trackVisibility: true, delay: 100}).observe(target);
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion.html
new file mode 100644
index 0000000..659a270d
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-radius-shadow-occlusion.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<meta name="viewport" content="width=device-width,initial-scale=1">
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<style>
+  body {
+    margin: 0;
+  }
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: red;
+  }
+  #occluder {
+    position: absolute;
+    left: -20px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    border-radius: 1px;
+    box-shadow: 120px 0px 0px black;
+    background: blue;
+  }
+</style>
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    // The target should be occluded by the box-shadow of the occluder.
+    assert_false(entries[0].isVisible);
+    done();
+  }, {trackVisibility: true, delay: 100}).observe(target);
+</script>
Loading diff…

Original Bug Report

reported by [email protected]

Bypass of PEPC and IntersectionObserver V2 occlusion detection via border-radius

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: A hit-testing logic error in Blink allows elements with ‘border-radius’ to incorrectly return early during hit tests, bypassing visual overflow checks. This flaw can be exploited to hide sensitive elements like Permission elements behind opaque box-shadows without being detected as occluded.

Affected files:

  • third_party/blink/renderer/core/paint/box_fragment_painter.cc
  • third_party/blink/renderer/core/layout/layout_replaced.cc

Estimated timestamp from git blame: 2018-09-12

Summary

A potential order-of-operations vulnerability exists in Blink’s hit-testing implementation within BoxFragmentPainter::NodeAtPoint and LayoutReplaced::NodeAtPoint. When a hit test is performed with the kHitTestVisualOverflow flag (primarily used by IntersectionObserver V2 and Page Embedded Permission Control), the presence of a border-radius on an occluding element can cause the hit test to return false prematurely. This allows visual overflow, such as box-shadow, to occlude a target without being detected by the security mechanisms designed to prevent clickjacking.

Root Cause Analysis

In third_party/blink/renderer/core/paint/box_fragment_painter.cc, the NodeAtPoint function contains the following logic:

// Check border-shape and border-radius clipping.
if (style.HasBorderShape()) {
  // ... early return logic
} else if (style.HasBorderRadius() &&
           HitTestClippedOutByBorder(hit_test.location, physical_offset)) {
  return false; // Vulnerable early return
}

// ... later, visual overflow is evaluated
if (hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow()) {
  bounds_rect = InkOverflowIncludingFilters();
  // ...
}

When HitTestForOcclusion is called, it sets the kHitTestVisualOverflow flag to ensure that non-layout visual elements like shadows are considered occluders. However, if the occluding element has a border-radius, HitTestClippedOutByBorder is called. If the hit-test point lies outside the rounded border box (even if it lies within the shadow), the function returns false at line 2366. This bypasses the logic at line 2403 that would have expanded the hit area to include the box-shadow via InkOverflowIncludingFilters().

Potential Attack Scenario

An attacker could potentially trigger this vulnerability to bypass the security checks of the <permission> element (PEPC) or any feature relying on IntersectionObserver visibility tracking:

  1. Place a sensitive element (e.g., <permission type="geolocation">) on a page.
  2. Create a sibling ‘occluder’ element positioned slightly offset from the target.
  3. Apply border-radius: 1px and a large, opaque box-shadow to the occluder such that the shadow completely covers the target element.
  4. Blink’s occlusion detection (via HitTestForOcclusion) will traverse to the occluder but return false due to the border-radius early-return bug.
  5. The sensitive element will be marked as isVisible: true despite being visually hidden by the shadow.
  6. A user clicking the opaque shadow will unknowingly interact with the hidden permission element, potentially granting the site sensitive permissions.

Suggested Fix

The early return checks for border-radius and border-shape in NodeAtPoint should be bypassed if the kHitTestVisualOverflow flag is present in the HitTestRequest. This ensures that the hit test proceeds to evaluate the full ink overflow rect, including shadows and filters.

Note: These are potential steps and analysis based on code review; a functional proof-of-concept has not yet been executed.

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


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