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
Tracker517155893
Fix commiteaec3a7e656e (chromium/src) +75/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Files Changed

  • 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-shape-occlusion.html
  • third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-overflow-occlusion.html
From eaec3a7e656e66a45b420f089367b33085c8a2b4 Mon Sep 17 00:00:00 2001
From: Philip Rogers <[email protected]>
Date: Wed, 27 May 2026 20:09:20 -0700
Subject: [PATCH] Consider visual overflow when hit testing border-shape

This patch ensures we do not early-out before hit testing visual
overflow when hit testing for occlusion in the presence of
border-shape.

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

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 6dd360e..27abe67 100644
--- a/third_party/blink/renderer/core/paint/box_fragment_painter.cc
+++ b/third_party/blink/renderer/core/paint/box_fragment_painter.cc
@@ -2455,7 +2455,9 @@
     const Path outer_path = ComputeBorderShapeOuterPath(
         style, rect, box_fragment_.GetLayoutObject());
     if (!hit_test.location.Intersects(outer_path)) {
-      return false;
+      if (!hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow()) {
+        return false;
+      }
     }
   } else if (style.HasBorderRadius() &&
              HitTestClippedOutByBorder(hit_test.location, physical_offset)) {
diff --git a/third_party/blink/renderer/core/paint/paint_layer.cc b/third_party/blink/renderer/core/paint/paint_layer.cc
index 7e05284..c9acf69c 100644
--- a/third_party/blink/renderer/core/paint/paint_layer.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer.cc
@@ -1407,7 +1407,7 @@
     return nullptr;
   }
 
-  if (layout_object.StyleRef().HasBorderShape() &&
+  if (!is_occlusion_test && layout_object.StyleRef().HasBorderShape() &&
       layout_object.ShouldClipOverflowAlongBothAxis() &&
       HitTestClippedOutByBorderShape(transform_container,
                                      recursion_data.location)) {
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-occlusion.html
new file mode 100644
index 0000000..e1e8cc5
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-occlusion.html
@@ -0,0 +1,35 @@
+<!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>
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: blue;
+  }
+  #occluder {
+    position: absolute;
+    left: 300px;
+    top: 100px;
+    width: 10px;
+    height: 10px;
+    border-shape: circle(5px at 50% 50%);
+    box-shadow: -150px 0 0 100px black;
+    background: red;
+  }
+</style>
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    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-shape-overflow-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-overflow-occlusion.html
new file mode 100644
index 0000000..ac141c63
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-overflow-occlusion.html
@@ -0,0 +1,36 @@
+<!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>
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: blue;
+  }
+  #occluder {
+    position: absolute;
+    left: 300px;
+    top: 100px;
+    width: 10px;
+    height: 10px;
+    border-shape: circle(5px at 50% 50%);
+    box-shadow: -150px 0 0 100px black;
+    background: red;
+    overflow: hidden;
+  }
+</style>
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    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-shape-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-occlusion.html
new file mode 100644
index 0000000..e1e8cc5
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-occlusion.html
@@ -0,0 +1,35 @@
+<!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>
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: blue;
+  }
+  #occluder {
+    position: absolute;
+    left: 300px;
+    top: 100px;
+    width: 10px;
+    height: 10px;
+    border-shape: circle(5px at 50% 50%);
+    box-shadow: -150px 0 0 100px black;
+    background: red;
+  }
+</style>
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    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-shape-overflow-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-overflow-occlusion.html
new file mode 100644
index 0000000..ac141c63
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/border-shape-overflow-occlusion.html
@@ -0,0 +1,36 @@
+<!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>
+  #target {
+    position: absolute;
+    left: 100px;
+    top: 100px;
+    width: 100px;
+    height: 100px;
+    background: blue;
+  }
+  #occluder {
+    position: absolute;
+    left: 300px;
+    top: 100px;
+    width: 10px;
+    height: 10px;
+    border-shape: circle(5px at 50% 50%);
+    box-shadow: -150px 0 0 100px black;
+    background: red;
+    overflow: hidden;
+  }
+</style>
+<div id="target"></div>
+<div id="occluder"></div>
+
+<script>
+  setup({ single_test: true });
+  const target = document.getElementById("target");
+  new IntersectionObserver(entries => {
+    assert_false(entries[0].isVisible);
+    done();
+  }, {trackVisibility: true, delay: 100}).observe(target);
+</script>
Loading diff…

Original Bug Report

reported by [email protected]

Occlusion check bypass via border-shape with box-shadow in BoxFragmentPainter

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 potential logic flaw in BoxFragmentPainter::NodeAtPoint’s handling of CSS border-shape allows layerless elements with decorative outsets to bypass occlusion detection. When checking border-shape, the hit-testing branch immediately returns false without verifying if the request is checking visual overflow. This enables an attacker to visually obscure a target Page-Embedded Permission Control (PEPC) or IntersectionObserver-tracked element with a shadow while the engine incorrectly reports it as fully visible.

Affected files:

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

Estimated timestamp from git blame: 2026-01-29

Root Cause Analysis

There is a potential asymmetric logic flaw in BoxFragmentPainter::NodeAtPoint inside third_party/blink/renderer/core/paint/box_fragment_painter.cc when handling the CSSBorderShape feature compared to standard border-radius corners.

When standard border-radius clipping is processed, an early return of false (meaning the point is clipped out by the border) is gated on whether the hit-test request is querying visual overflow (IsHitTestVisualOverflow()). This ensures that decorative outsets (such as box-shadow or filters) are properly evaluated during occlusion/visibility tests.

However, the border-shape sibling block does not perform this check. When the hit-test location is outside the border-shape outer path (which represents the physical border box bounds and does not account for visual overflow), the function immediately returns false unconditionally.

Below is the relevant code structure in third_party/blink/renderer/core/paint/box_fragment_painter.cc:

  // Check border-shape and border-radius clipping.
  if (style.HasBorderShape()) {
    PhysicalRect rect(physical_offset, size);
    const Path outer_path = ComputeBorderShapeOuterPath(
        style, rect, box_fragment_.GetLayoutObject());
    if (!hit_test.location.Intersects(outer_path)) {
      return false; // Potential unconditional early return bypasses IsHitTestVisualOverflow()
    }
  } else if (style.HasBorderRadius() &&
             HitTestClippedOutByBorder(hit_test.location, physical_offset)) {
    if (!hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow()) { // Correctly gated
      return false;
    }
  }

If a visibility or occlusion hit-test (which utilizes IsHitTestVisualOverflow()) occurs, and the location falls within the element’s visual overflow (such as a large box-shadow or negative-offset spread) but outside its actual physical border shape, the unconditional return false short-circuits NodeAtPoint. This prevents execution from reaching the self hit-test logic that would otherwise expand the bounding box using InkOverflowIncludingFilters() and register the overlap.

Potential Impact

An attacker could potentially exploit this behavior to perform a clickjacking attack on a primary security surface. By overlaying a layerless, static inline-block element styled with border-shape and a large spread/offset box-shadow, they can visually cover a Page-Embedded Permission Control (PEPC) element (e.g. <permission> or <geolocation>) or an element monitored via IntersectionObserver (V2).

Because the occlusion detection hit-test incorrectly returns false (no overlap), the target element is reported as fully visible and unoccluded. This keeps the PEPC element clickable and enabled, allowing the attacker to trick the user into clicking on the obscuring shadow and inadvertently granting sensitive browser permissions.

Suggested Technical Steps to Trigger (Potential)

(Note: These are suggested/potential steps; our tooling currently does not have the ability to run code or execute a live proof of concept).

  1. Serve a page containing a target PEPC element (e.g. <permission type="geolocation">) and a layerless in-flow sibling element designed to overlap it via a visual shadow outset:
    <permission type="geolocation"></permission>
    <span style="display:inline-block; width:2px; height:2px; border-shape:circle; box-shadow:-140px 0 0 140px black;"></span>
    
  2. Monitor the target element using an IntersectionObserver configured with {trackVisibility: true, delay: 100} or click interaction states.
  3. The black shadow completely covers the permission button, yet the browser’s occlusion check evaluates the target’s visibility status as isVisible: true.
  4. If standard border-radius: 1px is used instead of border-shape: circle, the occlusion check correctly identifies the shadow overlap and changes visibility to false (confirming that the bypass is unique to the border-shape path).

Proposed Remediation

Align the border-shape clipping branch logic with the border-radius branch by gating the early return on whether the request is querying visual overflow:

  // Check border-shape and border-radius clipping.
  if (style.HasBorderShape()) {
    PhysicalRect rect(physical_offset, size);
    const Path outer_path = ComputeBorderShapeOuterPath(
        style, rect, box_fragment_.GetLayoutObject());
    if (!hit_test.location.Intersects(outer_path)) {
      if (!hit_test.result->GetHitTestRequest().IsHitTestVisualOverflow()) {
        return false;
      }
    }
  } else if (style.HasBorderRadius() && ...

Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8


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