CVE-2026-79016
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc |
modified |
Files Changed
third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.ccthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/reference/green-100x100.htmlthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-fillpaint.sub.htmlthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-sourcealpha.sub.htmlthird_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-strokepaint.sub.html
Patch
From 0230e12ab23670f4004ee67f48896e8a68a7131e Mon Sep 17 00:00:00 2001 From: Fredrik Söderquist <[email protected]> Date: Fri, 24 Jul 2026 03:49:43 -0700 Subject: [PATCH] Ensure FillPaint, StrokePaint and SourceAlpha sources are tainted These sources would not get the tainted flag set if the canvas was tainted. Fixed: 497854976 Change-Id: If8217e5acfb0106b2b58a7234011ccfc39d7a83a Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8121147 Reviewed-by: Philip Rogers <[email protected]> Commit-Queue: Fredrik Söderquist <[email protected]> Cr-Commit-Position: refs/heads/main@{#1667769} --- diff --git a/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc b/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc index 30dd5f7..4a81812 100644 --- a/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc +++ b/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc @@ -122,6 +122,14 @@ MakeGarbageCollected<PaintFilterEffect>( source_graphic->GetFilter(), *stroke_flags)); } + // If SourceGraphic is tainted, we assume that all other built-in effects are + // tainted as well. This is obviously true for SourceAlpha, and works out for + // all current users that pass values for {Fill,Stroke}Paint (i.e <canvas>). + if (source_graphic->OriginTainted()) { + for (auto& entry : builtin_effects_) { + entry.value->SetOriginTainted(); + } + } AddBuiltinEffects(); } diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/reference/green-100x100.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/reference/green-100x100.html new file mode 100644 index 0000000..a3ab3bb --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/reference/green-100x100.html @@ -0,0 +1,2 @@ +<!doctype html> +<div style="width: 100px; height: 100px; background-color: green"></div> diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-fillpaint.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-fillpaint.sub.html new file mode 100644 index 0000000..d614ef1 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-fillpaint.sub.html @@ -0,0 +1,30 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>SVG <feDisplacementMap> filter using a cross-origin FillPaint has no effect</title> +<script src="/common/reftest-wait.js"></script> +<link rel="match" href="reference/green-100x100.html"> +<canvas id="canvas" width="100" height="100"> +<svg width="0" height="0"> + <filter id="disp"> + <feDisplacementMap in="SourceGraphic" in2="FillPaint" scale="100" + yChannelSelector="R" xChannelSelector="G"/> + </filter> +</svg> +<script> + const img = new Image(); + img.onload = () => { + const context = canvas.getContext('2d'); + context.fillStyle = 'red'; + context.fillRect(0, 0, 100, 100); + + context.fillStyle = context.createPattern(img, 'repeat'); + context.strokeStyle = 'green'; + context.lineWidth = 50; + context.filter = 'url(#disp)'; + context.strokeRect(25, 25, 50, 50); + + takeScreenshot(); + }; + img.onerror = takeScreenshot; + img.src = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/grgr-256x256.png'; +</script> diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-sourcealpha.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-sourcealpha.sub.html new file mode 100644 index 0000000..46957b3 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-sourcealpha.sub.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>SVG <feDisplacementMap> filter using a tainted SourceAlpha has no effect</title> +<script src="/common/reftest-wait.js"></script> +<link rel="match" href="reference/green-100x100.html"> +<canvas id="canvas" width="100" height="100"> +<svg width="0" height="0"> + <filter id="disp"> + <feDisplacementMap in="SourceGraphic" in2="SourceAlpha" scale="100" + yChannelSelector="A"/> + </filter> +</svg> +<script> + const img = new Image(); + img.onload = () => { + const context = canvas.getContext('2d'); + context.drawImage(img, 0, 0); + context.fillStyle = 'red'; + context.fillRect(0, 0, 100, 100); + + context.fillStyle = 'green'; + context.filter = 'url(#disp)'; + context.fillRect(0, 0, 100, 100); + + takeScreenshot(); + }; + img.onerror = takeScreenshot; + img.src = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/grgr-256x256.png'; +</script> diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-strokepaint.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-strokepaint.sub.html new file mode 100644 index 0000000..d2e7b0d4 --- /dev/null +++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-strokepaint.sub.html @@ -0,0 +1,29 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<title>SVG <feDisplacementMap> filter using a cross-origin StrokePaint has no effect</title> +<script src="/common/reftest-wait.js"></script> +<link rel="match" href="reference/green-100x100.html"> +<canvas id="canvas" width="100" height="100"> +<svg width="0" height="0"> + <filter id="disp"> + <feDisplacementMap in="SourceGraphic" in2="StrokePaint" scale="100" + yChannelSelector="R" xChannelSelector="G"/> + </filter> +</svg> +<script> + const img = new Image(); + img.onload = () => { + const context = canvas.getContext('2d'); + context.fillStyle = 'red'; + context.fillRect(0, 0, 100, 100); + + context.strokeStyle = context.createPattern(img, 'repeat'); + context.fillStyle = 'green'; + context.filter = 'url(#disp)'; + context.fillRect(0, 0, 100, 100); + + takeScreenshot(); + }; + img.onerror = takeScreenshot; + img.src = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/grgr-256x256.png'; +</script>
Regression Test / PoC
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/reference/green-100x100.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/reference/green-100x100.html
new file mode 100644
index 0000000..a3ab3bb
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/reference/green-100x100.html
@@ -0,0 +1,2 @@
+<!doctype html>
+<div style="width: 100px; height: 100px; background-color: green"></div>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-fillpaint.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-fillpaint.sub.html
new file mode 100644
index 0000000..d614ef1
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-fillpaint.sub.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>SVG <feDisplacementMap> filter using a cross-origin FillPaint has no effect</title>
+<script src="/common/reftest-wait.js"></script>
+<link rel="match" href="reference/green-100x100.html">
+<canvas id="canvas" width="100" height="100">
+<svg width="0" height="0">
+ <filter id="disp">
+ <feDisplacementMap in="SourceGraphic" in2="FillPaint" scale="100"
+ yChannelSelector="R" xChannelSelector="G"/>
+ </filter>
+</svg>
+<script>
+ const img = new Image();
+ img.onload = () => {
+ const context = canvas.getContext('2d');
+ context.fillStyle = 'red';
+ context.fillRect(0, 0, 100, 100);
+
+ context.fillStyle = context.createPattern(img, 'repeat');
+ context.strokeStyle = 'green';
+ context.lineWidth = 50;
+ context.filter = 'url(#disp)';
+ context.strokeRect(25, 25, 50, 50);
+
+ takeScreenshot();
+ };
+ img.onerror = takeScreenshot;
+ img.src = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/grgr-256x256.png';
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-sourcealpha.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-sourcealpha.sub.html
new file mode 100644
index 0000000..46957b3
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-sourcealpha.sub.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>SVG <feDisplacementMap> filter using a tainted SourceAlpha has no effect</title>
+<script src="/common/reftest-wait.js"></script>
+<link rel="match" href="reference/green-100x100.html">
+<canvas id="canvas" width="100" height="100">
+<svg width="0" height="0">
+ <filter id="disp">
+ <feDisplacementMap in="SourceGraphic" in2="SourceAlpha" scale="100"
+ yChannelSelector="A"/>
+ </filter>
+</svg>
+<script>
+ const img = new Image();
+ img.onload = () => {
+ const context = canvas.getContext('2d');
+ context.drawImage(img, 0, 0);
+ context.fillStyle = 'red';
+ context.fillRect(0, 0, 100, 100);
+
+ context.fillStyle = 'green';
+ context.filter = 'url(#disp)';
+ context.fillRect(0, 0, 100, 100);
+
+ takeScreenshot();
+ };
+ img.onerror = takeScreenshot;
+ img.src = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/grgr-256x256.png';
+</script>
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-strokepaint.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-strokepaint.sub.html
new file mode 100644
index 0000000..d2e7b0d4
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/filters/svg-displacement-filter-tainted-strokepaint.sub.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<title>SVG <feDisplacementMap> filter using a cross-origin StrokePaint has no effect</title>
+<script src="/common/reftest-wait.js"></script>
+<link rel="match" href="reference/green-100x100.html">
+<canvas id="canvas" width="100" height="100">
+<svg width="0" height="0">
+ <filter id="disp">
+ <feDisplacementMap in="SourceGraphic" in2="StrokePaint" scale="100"
+ yChannelSelector="R" xChannelSelector="G"/>
+ </filter>
+</svg>
+<script>
+ const img = new Image();
+ img.onload = () => {
+ const context = canvas.getContext('2d');
+ context.fillStyle = 'red';
+ context.fillRect(0, 0, 100, 100);
+
+ context.strokeStyle = context.createPattern(img, 'repeat');
+ context.fillStyle = 'green';
+ context.filter = 'url(#disp)';
+ context.fillRect(0, 0, 100, 100);
+
+ takeScreenshot();
+ };
+ img.onerror = takeScreenshot;
+ img.src = 'https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/grgr-256x256.png';
+</script>
Original Bug Report
FeDisplacementMap timing side-channel bypass via untainted builtin SVG filter inputs
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: Blink’s SVG filter implementation potentially fails to propagate origin-taint status to builtin filter inputs like FillPaint, StrokePaint, and SourceAlpha. This oversight allows an attacker to bypass security restrictions on the <feDisplacementMap> filter intended to prevent timing side-channel attacks. By using a tainted canvas pattern as FillPaint for a displacement map, an attacker could extract cross-origin pixel data by measuring rendering time variations.
Affected files:
third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.ccthird_party/blink/renderer/platform/graphics/filters/fe_displacement_map.ccthird_party/blink/renderer/modules/canvas/canvas2d/canvas_rendering_context_2d_state.ccthird_party/blink/renderer/core/paint/filter_effect_builder.ccthird_party/blink/renderer/platform/graphics/filters/paint_filter_effect.ccthird_party/blink/renderer/platform/graphics/filters/source_alpha.cc
Estimated timestamp from git blame: 2020-02-18
Summary
A potential vulnerability in Blink’s SVG filter building logic allows an attacker to bypass the origin-taint check in the feDisplacementMap filter. This issue re-enables a timing side-channel attack where cross-origin pixel values can be extracted by observing variations in rendering performance, bypassing the defense originally implemented for crbug.com/778506.
Root Cause Analysis
The vulnerability stems from two related issues:
-
Untainted Builtin Filter Effects: In
SVGFilterBuilder::SVGFilterBuilder()(third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc), the builtin effects forFillPaint,StrokePaint, andSourceAlphaare created without propagating any origin-taint status:builtin_effects_.insert(FilterInputKeywords::SourceAlpha(), MakeGarbageCollected<SourceAlpha>(source_graphic)); if (fill_flags) { builtin_effects_.insert(FilterInputKeywords::FillPaint(), MakeGarbageCollected<PaintFilterEffect>( source_graphic->GetFilter(), *fill_flags)); }The
FilterEffectbase class defaultsorigin_tainted_tofalse. While the canvas context may be tainted (e.g., by settingfillStyleto a cross-originCanvasPattern), andSourceGraphicis correctly marked as tainted, the newly createdPaintFilterEffectobjects forFillPaintandStrokePaintare never explicitly marked as tainted. Consequently, theirOriginTainted()methods incorrectly returnfalse. -
Incomplete Security Check in
FEDisplacementMap: The Filter Effects Module Level 1 specification requiresfeDisplacementMapto act as a pass-through filter if either the first or second input is origin-tainted to prevent timing attacks. However, inFEDisplacementMap::CreateImageFilter()(third_party/blink/renderer/platform/graphics/filters/fe_displacement_map.cc), the implementation only checks the second input:// FEDisplacementMap must be a pass-through filter if // the origin is tainted. See: // https://drafts.fxtf.org/filter-effects/#fedisplacemnentmap-restrictions. if (InputEffect(1)->OriginTainted()) return color;It misses the check for
InputEffect(0)->OriginTainted().
Exploitation Scenario
If an attacker sets canvas.fillStyle to a cross-origin CanvasPattern and applies an SVG filter containing an <feDisplacementMap> element with in2="FillPaint", the cross-origin shader is carried into the FillPaint effect. Because FillPaint is incorrectly considered untainted, the security check in FEDisplacementMap is bypassed. The filter proceeds to use cross-origin pixel values as displacement vectors.
An attacker can perform a pixel-by-pixel extraction of cross-origin image content by measuring the time it takes to render a canvas with the displacement filter applied. Rendering timing varies depending on the displacement distance (driven by the victim pixel values), allowing for an information leak of cross-origin data (CWE-200).
Potential Reproduction Steps
Note: These are suggested steps to trigger the vulnerability. Tooling does not currently allow running arbitrary code to confirm the exploit.
- Host an attacker-controlled page with an inline SVG filter:
<svg> <filter id="f"> <feDisplacementMap in="SourceGraphic" in2="FillPaint" xChannelSelector="R" yChannelSelector="G" scale="500"/> </filter> </svg> - Load a cross-origin image without CORS:
img.src = 'https://victim.example/secret.png'; - Upon image load, create a pattern and set it as the canvas
fillStyle(this taints the canvas context):const ctx = canvas.getContext('2d'); const p = ctx.createPattern(img, 'repeat'); ctx.fillStyle = p; ctx.filter = 'url(#f)'; - Repeatedly call
ctx.fillRect(0,0,1,1)while varyingSourceGraphicprobe patterns and measuring execution time usingperformance.now(). - The render timing will depend on the R/G channel values of the cross-origin image at the displacement point. An attacker can binary-search for specific pixel values based on these timing variations.
Suggested Fix
- Propagate Taint Status to Builtin Effects: Update the
SVGFilterBuilderconstructor to mark theSourceAlpha,FillPaint, andStrokePainteffects as tainted if the incomingsource_graphicis tainted or if the context flags contain cross-origin data. Alternatively, ensure thePaintFilterEffectitself evaluates taint based on the provided flags. - Enforce Complete Security Check: Correct the implementation in
FEDisplacementMap::CreateImageFilter()to check both inputs as required by the specification:if (InputEffect(0)->OriginTainted() || InputEffect(1)->OriginTainted()) return color;
Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0
Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.