Chrome · Paint
CVE-2026-17885
Logic Error in Paint
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/paint/scoped_svg_paint_state.cc |
modified | |
promise_testthird_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.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-filter.tentative.https.sub.html
Patch
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.
References
On This Page