CVE-2026-14013
Overview
Files Changed
third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.ccthird_party/blink/web_tests/external/wpt/intersection-observer/v2/svg-drop-shadow-occlusion.html
Patch
From 6edfe8111eb83953226e4fe540ecb0cc971cb3e3 Mon Sep 17 00:00:00 2001 From: Philip Rogers <[email protected]> Date: Wed, 27 May 2026 20:10:32 -0700 Subject: [PATCH] Skip overflow clip for SVG viewport containers doing occlusion tests This patch skips overflow clips when hit testing SVG viewport containers for occlusion. This ensures that we consider visual overflow for these occlusion hit tests. Fixed: 517114175 Change-Id: I6394e4b6dcbd4535eb70c695f46b98f942f8c39c Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7880272 Reviewed-by: Stefan Zager <[email protected]> Commit-Queue: Philip Rogers <[email protected]> Cr-Commit-Position: refs/heads/main@{#1637426} --- diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.cc b/third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.cc index 4ae24a3..aabf187f 100644 --- a/third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.cc +++ b/third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.cc @@ -23,6 +23,7 @@ #include "third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.h" #include "third_party/blink/renderer/core/layout/hit_test_location.h" +#include "third_party/blink/renderer/core/layout/hit_test_result.h" #include "third_party/blink/renderer/core/layout/svg/svg_layout_info.h" #include "third_party/blink/renderer/core/layout/svg/svg_layout_support.h" #include "third_party/blink/renderer/core/layout/svg/transform_helper.h" @@ -95,7 +96,8 @@ HitTestPhase phase) { NOT_DESTROYED(); // Respect the viewport clip which is in parent coordinates. - if (SVGLayoutSupport::IsOverflowHidden(*this)) { + if (SVGLayoutSupport::IsOverflowHidden(*this) && + !result.GetHitTestRequest().IsHitTestVisualOverflow()) { TransformedHitTestLocation local_transformed_hit_location( hit_test_location, LocalSVGTransform()); diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/svg-drop-shadow-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/svg-drop-shadow-occlusion.html new file mode 100644 index 0000000..57ae9fa --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/svg-drop-shadow-occlusion.html @@ -0,0 +1,41 @@ +<!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: 0; + top: 0; + width: 100px; + height: 40px; + background: lime; + } + #outer { + position: fixed; + left: 0; + top: 0; + width: 300px; + height: 100px; + visibility: hidden; + } + #inner { + visibility: visible; + filter: drop-shadow(-200px 0 0 black); + } +</style> +<div id="target"></div> +<svg id="outer"> + <svg id="inner" x="200" y="0" width="100" height="40"> + <rect width="100%" height="100%" fill="black"/> + </svg> +</svg> + +<script> + setup({ single_test: true }); + const target = document.getElementById("target"); + new IntersectionObserver(entries => { + assert_false(entries[0].isVisible, "Target should be occluded by SVG drop-shadow"); + done(); + }, {trackVisibility: true, delay: 100}).observe(target); +</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/intersection-observer/v2/svg-drop-shadow-occlusion.html b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/svg-drop-shadow-occlusion.html
new file mode 100644
index 0000000..57ae9fa
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/intersection-observer/v2/svg-drop-shadow-occlusion.html
@@ -0,0 +1,41 @@
+<!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: 0;
+ top: 0;
+ width: 100px;
+ height: 40px;
+ background: lime;
+ }
+ #outer {
+ position: fixed;
+ left: 0;
+ top: 0;
+ width: 300px;
+ height: 100px;
+ visibility: hidden;
+ }
+ #inner {
+ visibility: visible;
+ filter: drop-shadow(-200px 0 0 black);
+ }
+</style>
+<div id="target"></div>
+<svg id="outer">
+ <svg id="inner" x="200" y="0" width="100" height="40">
+ <rect width="100%" height="100%" fill="black"/>
+ </svg>
+</svg>
+
+<script>
+ setup({ single_test: true });
+ const target = document.getElementById("target");
+ new IntersectionObserver(entries => {
+ assert_false(entries[0].isVisible, "Target should be occluded by SVG drop-shadow");
+ done();
+ }, {trackVisibility: true, delay: 100}).observe(target);
+</script>
Original Bug Report
Clickjacking bypass via nested SVG viewport hit-test omission of visual overflow
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 LayoutSVGViewportContainer::NodeAtPoint allows attackers to bypass clickjacking defenses such as IntersectionObserver V2 and Page-Embedded Permission Control (PEPC). When overflow: hidden is active on a nested SVG, hit-test queries outside its viewport are rejected without verifying if they seek visual overflow. Consequently, opaque visual overflow (e.g., from a CSS drop-shadow filter) can obscure target elements while remaining completely invisible to the browser’s occlusion tests.
Affected files:
third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.cc
Estimated timestamp from git blame: 2018-09-28
Potential Clickjacking Bypass via Nested SVG Viewport Container
Description of the Issue
In Chromium’s rendering engine (Blink), elements that implement click-gating protections (like the Page-Embedded Permission Control / PEPC elements or IntersectionObserver V2 visibility tracking) utilize occlusion hit-testing to verify that they are not covered by any opaque overlays. This occlusion checking is done via LayoutObject::HitTestForOcclusion (third_party/blink/renderer/core/layout/layout_object.cc), which populates the HitTestRequest with the HitTestRequest::kHitTestVisualOverflow flag.
However, there is a potential logic flaw in how nested SVGs handle hit-testing queries. In third_party/blink/renderer/core/layout/svg/layout_svg_viewport_container.cc, LayoutSVGViewportContainer::NodeAtPoint immediately returns false if overflow: hidden is active on the element and the hit-test location is outside its layout viewport_ rectangle:
bool LayoutSVGViewportContainer::NodeAtPoint(
HitTestResult& result,
const HitTestLocation& hit_test_location,
const PhysicalOffset& accumulated_offset,
HitTestPhase phase) {
NOT_DESTROYED();
// Respect the viewport clip which is in parent coordinates.
if (SVGLayoutSupport::IsOverflowHidden(*this)) {
TransformedHitTestLocation local_transformed_hit_location(
hit_test_location, LocalSVGTransform());
if (!local_transformed_hit_location ||
!local_transformed_hit_location->Intersects(viewport_)) {
return false;
}
}
return LayoutSVGTransformableContainer::NodeAtPoint(
result, hit_test_location, accumulated_offset, phase);
}
Unlike other layout elements (such as those checked in BoxFragmentPainter::NodeAtPoint), this function fails to check if the incoming HitTestRequest specifies IsHitTestVisualOverflow(). This omission enables visual overflow, such as a CSS filter: drop-shadow, to paint opaque, attacker-controlled pixels outside of the viewport boundary, obscuring a target element while keeping the target visible and active from the browser’s occlusion hit-testing perspective.
Potential Step-by-Step Attack Scenario
Since our tooling does not currently have the capability to run code, the following sequence represents a potential/suggested scenario to trigger the issue:
- Setup Target: A sensitive target (such as a
<permission type="camera">element) is placed at coordinates(0,0,200,40)and initiates its clickjacking visibility monitoring viaIntersectionObserverandHTMLCapabilityElementBase. - Deploy Nested SVG Overlay: An attacker overlays a nested
<svg>container (SVGViewportContainerElement) positioned atx="500" y="0" width="200" height="40"with defaultoverflow: hiddenproperties. - Apply Visual Filter: The nested SVG container is styled with
visibility: visibleandfilter: drop-shadow(-500px 0 0 black). This renders an opaque copy of the SVG content at offset(0, 0, 200, 40), visually masking the target element under a black box. - Occlusion Check Omission: The browser performs
HitTestForOcclusionat the target coordinate(0, 0). When the traversal reaches the nested SVG,LayoutSVGViewportContainer::NodeAtPointrejects the hit-test coordinate early because(0,0)does not intersect its parent layoutviewport_(500, 0, 200, 40). - State Exploitation: The target element erroneously concludes it is fully visible and clickable. A user clicking the decoy black box instead registers a click on the sensitive permission element, potentially granting camera access.
Suggested Fix
To resolve this issue, LayoutSVGViewportContainer::NodeAtPoint should check if the hit-test request targets visual overflow. If IsHitTestVisualOverflow() is active, the early return for out-of-viewport locations should be bypassed, allowing the hit-test engine to evaluate child and visual filter bounds correctly:
// Respect the viewport clip which is in parent coordinates.
if (SVGLayoutSupport::IsOverflowHidden(*this) &&
!result.GetHitTestRequest().IsHitTestVisualOverflow()) {
TransformedHitTestLocation local_transformed_hit_location(
hit_test_location, LocalSVGTransform());
if (!local_transformed_hit_location ||
!local_transformed_hit_location->Intersects(viewport_)) {
return false;
}
}
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.