Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in Paint
DescriptionInappropriate implementation in Paint
ComponentPaint
Bug ClassLogic Error
Tracker523698038
Fix commitbbe4ea0264d8 (chromium/src) +74/-3
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
modified
promise_test
third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
modified

Files Changed

  • third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
From bbe4ea0264d886f0c3bcbd40636550ca63880bd2 Mon Sep 17 00:00:00 2001
From: Stefan Zager <szager@chromium.org>
Date: Sat, 27 Jun 2026 11:16:52 -0700
Subject: [PATCH] [HiC] Correctly invalidate SVG filters for privacy-preserving paint

Bug: 523698038
Change-Id: I5b2288867d1cfa0e5eb1a39678b7af76577a248f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8012950
Reviewed-by: Stephen Chenney <schenney@chromium.org>
Commit-Queue: Stefan Zager <szager@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1653688}
---

diff --git a/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc b/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
index d6f87251..abbc0cc5 100644
--- a/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
+++ b/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
@@ -146,10 +146,9 @@
   const auto* filter = properties.Filter();
   if (filter && filter->Filter() && filter->Filter()->OriginTainted() &&
       (paint_info_.GetPaintFlags() & PaintFlag::kPrivacyPreserving)) {
+    state.SetEffect(*filter->Parent());
     filter = nullptr;
-  }
-
-  if (filter) {
+  } else if (filter) {
     state.SetEffect(*filter);
   } else if (const auto* effect = properties.Effect()) {
     state.SetEffect(*effect);
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
new file mode 100644
index 0000000..5ae47f1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>drawElementImage does not use cross-origin feImages on filtered SVG text inside masks</title>
+  <link rel="help" href="https://github.com/WICG/html-in-canvas">
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+</head>
+
+<body>
+  <svg width="0" height="0">
+    <defs>
+      <filter id="filter-same" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <feImage href="https://{{location[host]}}/images/green-100x100.png"/>
+        <feComponentTransfer>
+          <feFuncR type="linear" slope="0" intercept="1"/>
+          <feFuncG type="linear" slope="0" intercept="1"/>
+          <feFuncB type="linear" slope="0" intercept="1"/>
+        </feComponentTransfer>
+      </filter>
+      <filter id="filter-cross" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <feImage href="https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png"/>
+        <feComponentTransfer>
+          <feFuncR type="linear" slope="0" intercept="1"/>
+          <feFuncG type="linear" slope="0" intercept="1"/>
+          <feFuncB type="linear" slope="0" intercept="1"/>
+        </feComponentTransfer>
+      </filter>
+      <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <text x="0" y="50" filter="url(#filter-same)">x</text>
+      </mask>
+      <mask id="mask-cross" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <text x="0" y="50" filter="url(#filter-cross)">x</text>
+      </mask>
+    </defs>
+  </svg>
+
+  <canvas id="canvas" width="100" height="200" layoutsubtree>
+    <svg id="child" width="100" height="200">
+      <svg x="0" y="0" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(30, 130, 230)" mask="url(#mask-same)"/>
+      </svg>
+      <svg x="0" y="100" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(40, 140, 240)" mask="url(#mask-cross)"/>
+      </svg>
+    </svg>
+  </canvas>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        var ctx = canvas.getContext('2d');
+        ctx.fillStyle = 'blue';
+        ctx.fillRect(0, 0, 100, 200);
+        ctx.drawElementImage(document.getElementById('child'), 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+        let pixel = _getPixelFromImageData(imgData, 50, 50);
+        assert_array_equals(pixel, [30, 130, 230, 255], "Same origin filter image in external mask should be drawn");
+
+        pixel = _getPixelFromImageData(imgData, 50, 150);
+        assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin filter image in external mask should not be drawn");
+      });
+    }
+  </script>
+</body>
+</html>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
new file mode 100644
index 0000000..5ae47f1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>drawElementImage does not use cross-origin feImages on filtered SVG text inside masks</title>
+  <link rel="help" href="https://github.com/WICG/html-in-canvas">
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+  <script src='/html/canvas/resources/canvas-tests.js'></script>
+  <script src="/html/canvas/resources/wait-for-canvas-paint.js"></script>
+</head>
+
+<body>
+  <svg width="0" height="0">
+    <defs>
+      <filter id="filter-same" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <feImage href="https://{{location[host]}}/images/green-100x100.png"/>
+        <feComponentTransfer>
+          <feFuncR type="linear" slope="0" intercept="1"/>
+          <feFuncG type="linear" slope="0" intercept="1"/>
+          <feFuncB type="linear" slope="0" intercept="1"/>
+        </feComponentTransfer>
+      </filter>
+      <filter id="filter-cross" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <feImage href="https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png"/>
+        <feComponentTransfer>
+          <feFuncR type="linear" slope="0" intercept="1"/>
+          <feFuncG type="linear" slope="0" intercept="1"/>
+          <feFuncB type="linear" slope="0" intercept="1"/>
+        </feComponentTransfer>
+      </filter>
+      <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <text x="0" y="50" filter="url(#filter-same)">x</text>
+      </mask>
+      <mask id="mask-cross" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <text x="0" y="50" filter="url(#filter-cross)">x</text>
+      </mask>
+    </defs>
+  </svg>
+
+  <canvas id="canvas" width="100" height="200" layoutsubtree>
+    <svg id="child" width="100" height="200">
+      <svg x="0" y="0" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(30, 130, 230)" mask="url(#mask-same)"/>
+      </svg>
+      <svg x="0" y="100" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(40, 140, 240)" mask="url(#mask-cross)"/>
+      </svg>
+    </svg>
+  </canvas>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        var ctx = canvas.getContext('2d');
+        ctx.fillStyle = 'blue';
+        ctx.fillRect(0, 0, 100, 200);
+        ctx.drawElementImage(document.getElementById('child'), 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+        let pixel = _getPixelFromImageData(imgData, 50, 50);
+        assert_array_equals(pixel, [30, 130, 230, 255], "Same origin filter image in external mask should be drawn");
+
+        pixel = _getPixelFromImageData(imgData, 50, 150);
+        assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin filter image in external mask should not be drawn");
+      });
+    }
+  </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.