Medium chrome Logic Error 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in SVG
DescriptionInappropriate implementation in SVG
ComponentSVG
Bug ClassLogic Error
Tracker476646486
Fix commit8b262aaf8d8d (chromium/src) +102/-50
CISA KEVNot listed
CreditedLyra Rebane (rebane2001)
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
while
third_party/blink/renderer/core/paint/embedded_content_painter.cc
modified
if
third_party/blink/renderer/core/paint/embedded_content_painter.cc
modified

Files Changed

  • testing/variations/fieldtrial_testing_config.json
  • third_party/blink/renderer/core/paint/embedded_content_painter.cc
  • third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
From 8b262aaf8d8dc9b87d83c346e02fe06f729ecd54 Mon Sep 17 00:00:00 2001
From: Ari Chivukula <arichiv@chromium.org>
Date: Wed, 27 May 2026 11:47:33 -0700
Subject: [PATCH] [SVG] (5) Extend SVG filter blocking to restricted local frames

As outlined in: https://lyra.horse/blog/2025/12/svg-clickjacking/ SVG
filters applied to frames/plugins contain a risk cross-origin
information extraction.

This CL extends protection to Android and other platforms where local
frames can be cross-origin or sandboxed (unlike desktop, where such
frames are always remote).

This CL is part of a series of CLs:
(1) UKM filters applied to embedded content
(2) UMA filters applied to embedded content
(3) Note filter addition or removal across will-change hints
(4) Add flag to block filters applied to embedded content
(5) Extend SVG filter blocking to restricted local frames

Bug: 476646486
Fixed: 507859641
Change-Id: Id70a3c309e2795373aa46d8decc5aed308a604dc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7848691
Commit-Queue: Ari Chivukula <arichiv@chromium.org>
Auto-Submit: Ari Chivukula <arichiv@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1637127}
---

diff --git a/testing/variations/fieldtrial_testing_config.json b/testing/variations/fieldtrial_testing_config.json
index bc9de7b..d4f5db9 100644
--- a/testing/variations/fieldtrial_testing_config.json
+++ b/testing/variations/fieldtrial_testing_config.json
@@ -19419,6 +19419,7 @@
                 {
                     "name": "Enabled",
                     "params": {
+                        "PreventSvgFilterPaintOnLocalFrameRestricted": "true",
                         "PreventSvgFilterPaintOnRemoteFrame": "true",
                         "PreventSvgFilterPaintOnWebPlugin": "true"
                     },
diff --git a/third_party/blink/renderer/core/paint/embedded_content_painter.cc b/third_party/blink/renderer/core/paint/embedded_content_painter.cc
index d2d26e3..f5132f4 100644
--- a/third_party/blink/renderer/core/paint/embedded_content_painter.cc
+++ b/third_party/blink/renderer/core/paint/embedded_content_painter.cc
@@ -18,6 +18,7 @@
 #include "third_party/blink/renderer/core/paint/object_painter.h"
 #include "third_party/blink/renderer/core/paint/paint_info.h"
 #include "third_party/blink/renderer/core/paint/paint_layer.h"
+#include "third_party/blink/renderer/core/paint/paint_property_tree_builder.h"
 #include "third_party/blink/renderer/core/paint/replaced_painter.h"
 #include "third_party/blink/renderer/core/paint/scrollable_area_painter.h"
 #include "third_party/blink/renderer/core/view_transition/view_transition.h"
@@ -102,16 +103,6 @@
   }
 }
 
-BASE_FEATURE(kPreventSvgFilterPaint, base::FEATURE_DISABLED_BY_DEFAULT);
-BASE_FEATURE_PARAM(bool,
-                   kPreventSvgFilterPaintOnRemoteFrame,
-                   &kPreventSvgFilterPaint,
-                   false);
-BASE_FEATURE_PARAM(bool,
-                   kPreventSvgFilterPaintOnWebPlugin,
-                   &kPreventSvgFilterPaint,
-                   false);
-
 // static
 std::optional<ScopedPaintChunkProperties>
 EmbeddedContentPainter::RemoveSvgFilterPaint(
@@ -119,26 +110,34 @@
     const PaintInfo& paint_info) {
   // First, we gate the removal of the reference filter paint behind a feature
   // flag. This is differentiated per-type of embedded content.
-  if (!layout_embedded_content.GetEmbeddedContentView() ||
-      !base::FeatureList::IsEnabled(kPreventSvgFilterPaint)) {
+  const EmbeddedContentView* embedded_content_view =
+      layout_embedded_content.GetEmbeddedContentView();
+  if (!embedded_content_view ||
+      !base::FeatureList::IsEnabled(features::kPreventSvgFilterPaint)) {
     return std::nullopt;
   }
   DisplayItem::Type display_item_type = DisplayItem::kUninitializedType;
-  switch (layout_embedded_content.GetEmbeddedContentView()
-              ->SvgFilterPaintedCounter()) {
+  switch (embedded_content_view->SvgFilterPaintedCounter()) {
     case mojom::blink::WebFeature::kSvgFilterPaintedOnLocalFrame:
-      // We are not disabling svg filters on local frames at this time, and even
-      // if we did want to we could not accomplish that goal here as they are
-      // not composited the same way.
-      return std::nullopt;
+      // We only care about restricted local frames.
+      if (!features::kPreventSvgFilterPaintOnLocalFrameRestricted.Get() ||
+          !To<LocalFrameView>(embedded_content_view)
+               ->GetFrame()
+               .IsCrossOriginToParentOrOuterDocument()) {
+        return std::nullopt;
+      }
+      // We cannot remove the filter here as that must be done during pre-paint
+      // in PaintPropertyTreeBuilder::SetupContextForFrame, but we still want to
+      // call CountDeprecation below, so we keep kUninitializedType as the type.
+      break;
     case mojom::blink::WebFeature::kSvgFilterPaintedOnRemoteFrame:
-      if (!kPreventSvgFilterPaintOnRemoteFrame.Get()) {
+      if (!features::kPreventSvgFilterPaintOnRemoteFrame.Get()) {
         return std::nullopt;
       }
       display_item_type = DisplayItem::kForeignLayerRemoteFrame;
       break;
     case mojom::blink::WebFeature::kSvgFilterPaintedOnWebPlugin:
-      if (!kPreventSvgFilterPaintOnWebPlugin.Get()) {
+      if (!features::kPreventSvgFilterPaintOnWebPlugin.Get()) {
         return std::nullopt;
       }
       display_item_type = DisplayItem::kWebPlugin;
@@ -147,31 +146,26 @@
       NOTREACHED();
   }
 
-  // First we iterate the effect tree looking for the nearest parent effect
-  // without a reference filter applied to any of its parents.
-  const blink::EffectPaintPropertyNode* current_effect =
-      &paint_info.context.GetPaintController()
-           .CurrentPaintChunkProperties()
-           .Effect()
-           .Unalias();
-  const blink::EffectPaintPropertyNode* candidate_effect = nullptr;
-  while (current_effect) {
-    const blink::EffectPaintPropertyNode* next_effect =
-        current_effect->UnaliasedParent();
-    if (current_effect->HasReferenceFilter()) {
-      candidate_effect = next_effect;
-    }
-    current_effect = next_effect;
-  }
+  // Then find the parent effect to overwrite with (if it exists).
+  const blink::EffectPaintPropertyNode* candidate_effect =
+      PaintPropertyTreeBuilder::GetFirstParentEffectWithoutReferenceFilter(
+          &paint_info.context.GetPaintController()
+               .CurrentPaintChunkProperties()
+               .Effect());
 
   // We can exit early if there is no effect with a reference filter.
   if (!candidate_effect) {
     return std::nullopt;
   }
-
-  // Finally we emplace the (revised) scoped properties.
   layout_embedded_content.GetDocument().CountDeprecation(
       mojom::blink::WebFeature::kPreventSvgFilterPaint);
+
+  // We can exit at this point if we don't have a targetable type.
+  if (display_item_type == DisplayItem::kUninitializedType) {
+    return std::nullopt;
+  }
+
+  // Finally we emplace the (revised) scoped properties.
   return std::optional<ScopedPaintChunkProperties>{
       std::in_place, paint_info.context.GetPaintController(), *candidate_effect,
       layout_embedded_content, display_item_type};
diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
index 980e468..b8b6efe 100644
--- a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
+++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
@@ -102,6 +102,24 @@
 
 namespace blink {
 
+namespace features {
+
+BASE_FEATURE(kPreventSvgFilterPaint, base::FEATURE_DISABLED_BY_DEFAULT);
+BASE_FEATURE_PARAM(bool,
+                   kPreventSvgFilterPaintOnLocalFrameRestricted,
+                   &kPreventSvgFilterPaint,
+                   false);
+BASE_FEATURE_PARAM(bool,
+                   kPreventSvgFilterPaintOnRemoteFrame,
+                   &kPreventSvgFilterPaint,
+                   false);
+BASE_FEATURE_PARAM(bool,
+                   kPreventSvgFilterPaintOnWebPlugin,
+                   &kPreventSvgFilterPaint,
+                   false);
+
+}  // namespace features
+
 namespace {
 
 // This function is for convenience of debugging. For example, we can set a
@@ -193,6 +211,17 @@
   PaintPropertyTreeBuilderFragmentContext& context =
       full_context.fragment_context;
 
+  // Potentially disable svg filter applied to restricted local frame.
+  if (base::FeatureList::IsEnabled(features::kPreventSvgFilterPaint) &&
+      features::kPreventSvgFilterPaintOnLocalFrameRestricted.Get() &&
+      frame_view.GetFrame().IsCrossOriginToParentOrOuterDocument()) {
+    const blink::EffectPaintPropertyNode* candidate_effect =
+        GetFirstParentEffectWithoutReferenceFilter(context.current_effect);
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials b/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
index 66b8e29..7e64fd63 100644
--- a/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
+++ b/third_party/blink/web_tests/FlagExpectations/disable-site-isolation-trials
@@ -177,7 +177,6 @@
 [ Linux ] external/wpt/clear-site-data/clear-cache.https.html [ Failure ]  # Flaky output
 
 # See crbug.com/476646486: Requires site isolation
-virtual/prevent-svg-filter-paint/http/tests/inspector-protocol/issues/deprecation-prevent-svg-filter-paint.js [ Skip ]
 virtual/prevent-svg-filter-paint/wpt_internal/svg/svg-filter-render.sub.https.html [ Skip ]
 virtual/prevent-svg-filter-paint/wpt_internal/svg/use-count-svg-filter-nested-iframe.sub.https.html [ Skip ]
 virtual/prevent-svg-filter-paint/wpt_internal/svg/use-count-svg-filter-remote-iframe.sub.https.html [ Skip ]
diff --git a/third_party/blink/web_tests/TestLists/content_shell.filter b/third_party/blink/web_tests/TestLists/content_shell.filter
index c299eab..2b5970d 100644
--- a/third_party/blink/web_tests/TestLists/content_shell.filter
+++ b/third_party/blink/web_tests/TestLists/content_shell.filter
@@ -928,14 +928,5 @@
 virtual/webnn-service-with-gpu/*
 virtual/lna-webrtc/wpt_internal/*
 
-# See crbug.com/476646486:
-# > Requires content plugin:
+# Requires content plugin, see crbug.com/476646486:
 virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-web-plugin.tentative.https.html
-# > Requires site isolation:
-virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-cross-origin-frame-in-cross-origin-frame.tentative.sub.https.html
-virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-cross-origin-frame-in-same-origin-frame.tentative.https.html
-virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-cross-origin-frame.tentative.sub.https.html
-virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-cross-origin-navigation-in-srcdoc-frame.tentative.sub.https.html
-virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-same-origin-frame-in-cross-origin-frame.tentative.sub.https.html
-virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-same-origin-frame-reload-as-sandbox.tentative.https.html
-virtual/prevent-svg-filter-paint/external/wpt/svg/styling/svg-filter-render-sandbox-frame.tentative.https.html
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index bee19e1..c8cf41b 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -5344,7 +5344,7 @@
       "wpt_internal/svg/svg-filter-render.sub.https.html"
     ],
     "args": [
-      "--enable-features=PreventSvgFilterPaint:PreventSvgFilterPaintOnRemoteFrame/true/PreventSvgFilterPaintOnWebPlugin/true"
+      "--enable-features=PreventSvgFilterPaint:PreventSvgFilterPaintOnLocalFrameRestricted/true/PreventSvgFilterPaintOnRemoteFrame/true/PreventSvgFilterPaintOnWebPlugin/true"
     ],
     "expires": "Nov 1, 2026"
   },
Loading diff…

Original Bug Report

reported by re...@gmail.com

Cross-origin and local file:// read through SVG filters and QR codes

VULNERABILITY DETAILS
This report is basically my SVG clickjacking technique, but applied to the browser and local file paths.

Tl;dr of the technique is that SVG filters can read cross-origin pixels and perform operations on them.

If an attacker can convince a user to download a html file and scan a QR code within it, the contents of any text file can be exfiltrated and read by the attacker. For example, an attacker could create a fake order confirmation for a ticket, which has a QR code inside that must be scanned to add the ticket to Google Wallet. Upon scanning the QR code, the user’s private key is exfiltrated.

To prove that the attack is possible, I wrote an SVG filter that can generate a QR code that fits ~365 bytes of data. I also wrote a filter that can read the entire base64 character set off of an arbitrary text file inside an iframe.

I believe that with some work, this attack could read out an entire id_ed25519 private key file - the QR code is already big enough to fit its contents, and the character reading could be worked on further to read out the entire file instead of just the first 40 characters.

The QR code also scans really well on my phone’s stock camera app. On there it shows up as “example.com”, which when tapped opens the full url in the browser. This means that the user doesn’t even see the data in the URL until it’s already loaded (and sent to attacker). The data could of course be obfuscated too, I kept it as the same characters just for the demo.

My suggestion would be to make SVG filters not apply to cross-origin frames, which is what Webkit already does.

VERSION
Chrome Version: 145.0.7620.3 Dev
Operating System: Windows 10

REPRODUCTION CASE
Make sure you’re using Windows 10, no custom color profile (Automatically manage color for apps setting), and no display scaling (must be at 100%).

  1. Download the attached files into the same folder.
  2. Add your secret message into the secret file such that it matches [A-Za-z0-9+/]{40}.
  3. Open the fake-ticket-demo.html file directly in the browser, so that it opens on a file:// URI.
  4. Wait a moment, and then scan the QR code on the page. It will take you to https://example.com?ref=, followed by some spaces and the contents of the secret file in reverse.

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