Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient policy enforcement in Paint
DescriptionInsufficient policy enforcement in Paint
ComponentPaint
Bug ClassLogic Error
Tracker501606085
Fix commit27dafe4c2314 (chromium/src) +102/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/paint/paint_layer_painter.cc
modified
promise_test
third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
modified

Files Changed

  • third_party/blink/renderer/core/paint/paint_layer_painter.cc
  • third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc
  • third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h
  • third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h
  • third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
From 27dafe4c2314e5c9af29f3a95e8d94aec9e87eb3 Mon Sep 17 00:00:00 2001
From: Philip Rogers <pdr@chromium.org>
Date: Fri, 24 Apr 2026 17:27:13 -0700
Subject: [PATCH] [html-in-canvas] Support backdrop-filter images

This patch adds support for backdrop-filter images in 3 changes:
1. PaintLayerPainter::Paint creates a paint chunk for reference filter
effects, similar to regular filters.
2. EffectPaintPropertyNode::HasReferenceFilter has been updated to
return true for filter references and backdrop-filter references.
3. RasterInvalidator now expands bounds to cover the backdrop filter
bounds to cover cases where the backdrop filter effect has changed.

A followup patch will fix the cross-origin issue (see new test).

Bug: 501606085
Change-Id: Ibae248a3e5a735e142200b8a63af3556a82e9b8b
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7793231
Reviewed-by: Stefan Zager <szager@chromium.org>
Auto-Submit: Philip Rogers <pdr@chromium.org>
Commit-Queue: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1620574}
---

diff --git a/third_party/blink/renderer/core/paint/paint_layer_painter.cc b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
index 2f251d9..eb1c45b 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_painter.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
@@ -438,6 +438,8 @@
     auto* properties = object.FirstFragment().PaintProperties();
     ensure_chunk |= properties && properties->Filter() &&
                     properties->Filter()->HasReferenceFilter();
+    ensure_chunk |= properties && properties->Effect() &&
+                    properties->Effect()->HasReferenceFilter();
 
     if (ensure_chunk) {
       controller.EnsureChunk();
diff --git a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc
index 83caf3ec..90955dd2 100644
--- a/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc
+++ b/third_party/blink/renderer/platform/graphics/compositing/paint_chunks_to_cc_layer.cc
@@ -755,7 +755,7 @@
   current_clip_ = input_clip;
   current_effect_ = &effect;
 
-  if (effect.HasReferenceFilter()) {
+  if (effect.HasReferenceFilter() && effect.Filter()) {
     // For empty chunks, or chunks with empty bounds, with a filter applied
     // that produces output even when there's no input this will expand the
     // bounds to match.
diff --git a/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h b/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h
index 79318633..1f58cf7 100644
--- a/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h
+++ b/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.h
@@ -262,8 +262,10 @@
   }
 
   bool HasReferenceFilter() const {
-    return state_.filter_info &&
-           state_.filter_info->operations.HasReferenceFilter();
+    return (state_.filter_info &&
+            state_.filter_info->operations.HasReferenceFilter()) ||
+           (state_.backdrop_filter_info &&
+            state_.backdrop_filter_info->operations.HasReferenceFilter());
   }
   bool HasFilterThatMovesPixels() const {
     return state_.filter_info &&
diff --git a/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h b/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h
index 9172d16..a6f1b971 100644
--- a/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h
+++ b/third_party/blink/renderer/platform/graphics/paint/raster_invalidator.h
@@ -16,6 +16,7 @@
 #include "third_party/blink/renderer/platform/heap/garbage_collected.h"
 #include "third_party/blink/renderer/platform/wtf/vector.h"
 #include "ui/gfx/geometry/rect.h"
+#include "ui/gfx/geometry/rect_conversions.h"
 #include "ui/gfx/geometry/transform.h"
 
 namespace blink {
@@ -81,6 +82,15 @@
               mapper.MapVisualRect(chunk_it->drawable_bounds))),
           chunk_to_layer_clip(mapper.ClipRect()),
           chunk_to_layer_transform(mapper.Transform()) {
+      if (chunk_it->properties.Effect().Unalias().BackdropFilter()) {
+        gfx::RectF backdrop_rect =
+            gfx::SkRectToRectF(chunk_it->properties.Effect()
+                                   .Unalias()
+                                   .BackdropFilterBounds()
+                                   .getBounds());
+        bounds_in_layer.Union(invalidator.ClipByLayerBounds(
+            mapper.MapVisualRect(gfx::ToEnclosingRect(backdrop_rect))));
+      }
     }
 
     PaintChunkInfo(const PaintChunkInfo& old_chunk_info,
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
new file mode 100644
index 0000000..28ee93a0
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not use cross-origin feimage content via CSS backdrop-filter</title>
+  <link rel="help" href="https://github.com/WICG/html-in-canvas">
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <style>
+    #wrapper {
+      width: 10px;
+      height: 20px;
+      background: blue;
+    }
+    .child {
+      width: 10px;
+      height: 10px;
+      background: transparent;
+    }
+    #child-same { backdrop-filter: url('#filter-same'); }
+    #child-cross { backdrop-filter: url('#filter-cross'); }
+  </style>
+</head>
+<body>
+  <svg width="0" height="0">
+    <filter id="filter-same">
+      <feimage href="https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/green-100x100.png" />
+    </filter>
+    <filter id="filter-cross">
+      <feimage href="https://{{hosts[alt][www]}}:{{ports[h2][0]}}/wpt_internal/html/canvas/drawElementImage/resources/red-100x100.png" />
+    </filter>
+  </svg>
+
+  <canvas id=canvas width="10" height="20" layoutsubtree>
+    <div id="wrapper">
+      <div id="child-same" class="child"></div>
+      <div id="child-cross" class="child"></div>
+    </div>
+  </canvas>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        const preloadImage = (url) => new Promise((resolve, reject) => {
+          const img = new Image();
+          img.onload = () => resolve(img);
+          img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
+          img.src = url;
+        });
+
+        const sameOriginFeImageEl = document.querySelector('#filter-same feimage');
+        const crossOriginFeImageEl = document.querySelector('#filter-cross feimage');
+        await Promise.all([
+          preloadImage(sameOriginFeImageEl.getAttribute('href')),
+          preloadImage(crossOriginFeImageEl.getAttribute('href'))
+        ]);
+
+        await new Promise(requestAnimationFrame);
+        await new Promise(setTimeout);
+        var ctx = canvas.getContext('2d');
+        ctx.drawElementImage(document.getElementById('wrapper'), 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+
+        // Helper function to extract a pixel's RGBA array at (x, y).
+        const getPixel = (x, y) => {
+          const index = (y * canvas.width + x) * 4;
+          return [
+            imgData[index],
+            imgData[index + 1],
+            imgData[index + 2],
+            imgData[index + 3]
+          ];
+        };
+
+        let pixel = getPixel(5, 5);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Same origin feImage backdrop-filter should draw");
+        // TODO(https://crbug.com/501606085): Enable the cross-origin test.
+        // pixel = getPixel(5, 15);
+        // assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin feImage backdrop-filter should not draw");
+      });
+    }
+  </script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
new file mode 100644
index 0000000..28ee93a0
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/backdrop-filter-png-images-ignored.https.sub.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not use cross-origin feimage content via CSS backdrop-filter</title>
+  <link rel="help" href="https://github.com/WICG/html-in-canvas">
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <style>
+    #wrapper {
+      width: 10px;
+      height: 20px;
+      background: blue;
+    }
+    .child {
+      width: 10px;
+      height: 10px;
+      background: transparent;
+    }
+    #child-same { backdrop-filter: url('#filter-same'); }
+    #child-cross { backdrop-filter: url('#filter-cross'); }
+  </style>
+</head>
+<body>
+  <svg width="0" height="0">
+    <filter id="filter-same">
+      <feimage href="https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/green-100x100.png" />
+    </filter>
+    <filter id="filter-cross">
+      <feimage href="https://{{hosts[alt][www]}}:{{ports[h2][0]}}/wpt_internal/html/canvas/drawElementImage/resources/red-100x100.png" />
+    </filter>
+  </svg>
+
+  <canvas id=canvas width="10" height="20" layoutsubtree>
+    <div id="wrapper">
+      <div id="child-same" class="child"></div>
+      <div id="child-cross" class="child"></div>
+    </div>
+  </canvas>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        const preloadImage = (url) => new Promise((resolve, reject) => {
+          const img = new Image();
+          img.onload = () => resolve(img);
+          img.onerror = () => reject(new Error(`Failed to load image: ${url}`));
+          img.src = url;
+        });
+
+        const sameOriginFeImageEl = document.querySelector('#filter-same feimage');
+        const crossOriginFeImageEl = document.querySelector('#filter-cross feimage');
+        await Promise.all([
+          preloadImage(sameOriginFeImageEl.getAttribute('href')),
+          preloadImage(crossOriginFeImageEl.getAttribute('href'))
+        ]);
+
+        await new Promise(requestAnimationFrame);
+        await new Promise(setTimeout);
+        var ctx = canvas.getContext('2d');
+        ctx.drawElementImage(document.getElementById('wrapper'), 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+
+        // Helper function to extract a pixel's RGBA array at (x, y).
+        const getPixel = (x, y) => {
+          const index = (y * canvas.width + x) * 4;
+          return [
+            imgData[index],
+            imgData[index + 1],
+            imgData[index + 2],
+            imgData[index + 3]
+          ];
+        };
+
+        let pixel = getPixel(5, 5);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Same origin feImage backdrop-filter should draw");
+        // TODO(https://crbug.com/501606085): Enable the cross-origin test.
+        // pixel = getPixel(5, 15);
+        // assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin feImage backdrop-filter should not draw");
+      });
+    }
+  </script>
+</body>
+</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.