Medium watch CVSS 4.3 chrome Info Disclosure 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
4.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Canvas
DescriptionInformation leak in Canvas
ComponentCanvas
Sub-componentCanvas
Bug ClassInfo Disclosure
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Tracker501594511
Fix commitdcb779a5c5eb (chromium/src) +93/-4
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-04-11 (per vendor advisory)
Fix landed2026-07-24 (public git)
Disclosed2026-08-25
Patch gap32 days — fix public in source before the advisory

Background

Canvas origin-clean flag
a 2D canvas becomes “tainted” once privacy-sensitive content is drawn into it. After that, read-back APIs such as getImageData() throw a SecurityError (the behavior the regression test checks for).
`ctx.filter` vs `ctx.beginLayer({filter})`
two ways to apply CSS filter strings to 2D canvas drawing. Both accept url(...) references to SVG <filter> elements.
Tainted filter primitives
under the Filter Effects spec, some SVG filter inputs are privacy-sensitive, for example feImage of a cross-origin image or of a document element, or feFlood with currentColor. The commit message says these can reveal cross-origin pixels or colors that show whether a site was visited, so a filter that uses them must taint whatever it paints into.
`FilterEffect::OriginTainted()` / `SetOriginTaintedByContent()`
OriginTainted() reports whether the filter graph built by filter_effect_builder.BuildFilterEffect(...) contains tainted inputs. SetOriginTaintedByContent() marks the canvas as no longer origin-clean.

Root Cause Analysis

In canvas_2d_recorder_context.cc, the code that resolves the filter for beginLayer({filter}) built the FilterEffect and passed it straight into paint_filter_builder::Build(...) inline, so it never looked at whether the resulting filter graph was origin-tainted. The commit message says ctx.filter already handled this in CanvasRenderingContext2DState::GetFilter(), but that logic was missing for the beginLayer path. A layer filter that referenced a tainted SVG filter could therefore paint sensitive content (cross-origin image pixels, element snapshots, currentColor-derived colors) into a canvas that stayed origin-clean. The violated invariant is that any tainted input reaching the canvas bitmap must mark the canvas not origin-clean.

The fix stores the FilterEffect* in a local and, when filter_effect && filter_effect->OriginTainted() && !origin_tainted_by_content_, calls SetOriginTaintedByContent() before building the paint filter, which makes the later getImageData() throw. The enclosing function’s name is not shown in the hunk. Going by the commit title and the test, it is the filter-resolution logic used by beginLayer.

Key insight
The canvas tainting rule was implemented for only one of the two entry points that accept SVG reference filters (ctx.filter), and the parallel beginLayer({filter}) path dropped the filter’s taint bit. The fix checks FilterEffect::OriginTainted() on the layer path too and taints the canvas to match.

Attack Path

  1. Prepare a tainted SVG filter The attacker’s page defines an SVG <filter> whose primitives are privacy-sensitive, for example feImage pointing at a cross-origin image or at a page element, or feFlood with flood-color="currentColor", as in the regression test.
  2. Apply it through a canvas layer The page calls ctx.beginLayer({filter: 'url(#...)'}), draws, and calls ctx.endLayer(). The filter output is composited into the canvas bitmap.
  3. Taint not propagated Before the fix, the beginLayer filter path never consulted OriginTainted(), so the canvas stayed origin-clean.
  4. Read back sensitive pixels The page calls getImageData() (or another read-back API) and gets pixels derived from cross-origin or otherwise sensitive content instead of a SecurityError.
  5. Derive information The attacker infers cross-origin image content or visited-link and other style-dependent colors from the pixel values (inferred from the commit message’s description of what such filters can contain).

Impact Assessment

An ordinary web page can bypass the canvas same-origin read-back protection in the renderer, leaking cross-origin image pixels or privacy-sensitive colors such as visited-link state, as the commit message describes. No compromised renderer or user interaction is required, but CanvasRenderingContext2D.beginLayer() must be available in the build. The patch does not say whether that API is enabled by default. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 4.3.

Files Changed

  • third_party/blink/web_tests/wpt_internal/html/canvas/canvas-filter-url-reference-tainting.html
  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc

Audit Directions

  • Parallel entry points sharing a security rule
    Wherever one API path enforces taint, CORS or origin checks (here CanvasRenderingContext2DState::GetFilter()), list every sibling path that reaches the same sink, such as OffscreenCanvas and worker contexts, CanvasPattern, drawImage with filters, and createImageBitmap, and confirm each one propagates the taint bit.
  • Taint dropped when results are built inline
    Look for code that builds an object carrying security metadata (like FilterEffect::OriginTainted()) and immediately converts it to a lower-level representation (PaintFilter, cc::PaintRecord), losing the metadata before anyone checks it.
  • Other consumers of CSS `url()` filters
    Review other places where CSS filter strings with SVG references feed readable surfaces, such as WebGL or WebGPU texture uploads from canvas, toDataURL/toBlob, and captureStream, to confirm they respect the origin-clean state of filter outputs.
From dcb779a5c5ebc3a3ba5062f874c98e76c25fe0bd Mon Sep 17 00:00:00 2001
From: Jean-Philippe Gravel <[email protected]>
Date: Fri, 24 Jul 2026 16:24:20 -0700
Subject: [PATCH] [canvas] Propagate taint from beginLayer reference filters

The 2D canvas supports image filtering, either via `ctx.filter` or
`ctx.beginLayer({filter})`. Both approach supports CSS filter strings,
which supports `url(...)` references to SVG filters. As detailed in [1],
filters can potentially contain privacy-sensitive information, like
cross-origin images, or colors indicating whether a remote site was
visited. Such filters are tainted and that taint should propagate to the
canvas to prevent the page from reading back the image.

For ctx.filter, CanvasRenderingContext2DState::GetFilter() handles this
taint propagation. This logic was missing for ctx.beginLayer({filter}).

[1] https://drafts.csswg.org/filter-effects/#tainted-filter-primitives

Fixed: 501594511
Change-Id: Ie025d867d5b684b11627f247c4c47412a5f38149
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8143839
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Commit-Queue: Jean-Philippe Gravel <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1668214}
---
diff --git a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
index 133ae40..f5fe406 100644
--- a/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
+++ b/third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc
@@ -104,6 +104,7 @@
 #include "third_party/blink/renderer/platform/graphics/blend_mode.h"
 #include "third_party/blink/renderer/platform/graphics/canvas_2d_resource_provider.h"
 #include "third_party/blink/renderer/platform/graphics/color.h"
+#include "third_party/blink/renderer/platform/graphics/filters/filter_effect.h"
 #include "third_party/blink/renderer/platform/graphics/filters/paint_filter_builder.h"
 #include "third_party/blink/renderer/platform/graphics/gpu/webgpu_mailbox_texture.h"
 #include "third_party/blink/renderer/platform/graphics/graphics_context.h"
@@ -472,10 +473,14 @@
           1.0f,  // Deliberately ignore zoom on the canvas element.
           Color::kBlack, mojom::blink::ColorScheme::kLight);
 
-      filter = paint_filter_builder::Build(
-          filter_effect_builder.BuildFilterEffect(std::move(filter_operations),
-                                                  !OriginClean()),
-          kInterpolationSpaceSRGB);
+      FilterEffect* filter_effect = filter_effect_builder.BuildFilterEffect(
+          std::move(filter_operations), !OriginClean());
+      if (filter_effect && filter_effect->OriginTainted() &&
+          !origin_tainted_by_content_) {
+        SetOriginTaintedByContent();
+      }
+      filter =
+          paint_filter_builder::Build(filter_effect, kInterpolationSpaceSRGB);
     }
   }
 
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/canvas-filter-url-reference-tainting.html b/third_party/blink/web_tests/wpt_internal/html/canvas/canvas-filter-url-reference-tainting.html
new file mode 100644
index 0000000..50b2a8c2
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/canvas-filter-url-reference-tainting.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<title>CSS url() filter passed to beginLayer may taint the canvas</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<svg style="display: block; width: 0; height: 0">
+  <defs>
+    <!-- Not tainted. -->
+    <filter id="floodLime">
+      <feFlood flood-color="lime"/>
+    </filter>
+    <filter id="sameOriginImage">
+      <feImage href="/images/green.svg"/>
+    </filter>
+
+    <!-- Tainted. -->
+    <filter id="floodCurrentColor">
+      <feFlood flood-color="currentColor"/>
+    </filter>
+    <filter id="elementImage">
+      <feImage href="#circle"/>
+    </filter>
+    <filter id="crossOriginImage">
+      <feImage>
+        <script>
+          document.currentScript.parentElement.setAttribute(
+              'href', `${get_host_info().REMOTE_ORIGIN}/images/green.svg`);
+        </script>
+      </feImage>
+    </filter>
+    <circle id="circle" r="100" fill="lime"/>
+  </defs>
+</svg>
+
+<script type="module">
+await new Promise(resolve => window.addEventListener('load', resolve));
+
+// SVG reference filters taint the canvas according to
+// https://drafts.csswg.org/filter-effects/#tainted-filter-primitives
+
+// Not tainted.
+for (let filter of [
+  `url(${location.href}#floodLime)`,
+  `url(${location.href}#sameOriginImage)`,
+]) {
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.filter = filter;
+    ctx.fillRect(0, 0, 300, 150);
+    assert_array_equals(ctx.getImageData(1, 1, 1, 1).data, [0, 255, 0, 255]);
+  }, `Setting filter to '${filter}' should not taint the canvas.`);
+
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.beginLayer({filter});
+    ctx.fillRect(0, 0, 300, 150);
+    ctx.endLayer();
+    assert_array_equals(ctx.getImageData(1, 1, 1, 1).data, [0, 255, 0, 255]);
+  }, `Using a layer with filter '${filter}' should not taint the canvas.`);
+}
+
+// Tainted.
+for (let filter of [
+  `url(${location.href}#floodCurrentColor)`,
+  `url(${location.href}#elementImage)`,
+  `url(${location.href}#crossOriginImage)`,
+]) {
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.filter = filter;
+    ctx.fillRect(0, 0, 300, 150);
+    assert_throws_dom("SecurityError", () => ctx.getImageData(1, 1, 1, 1));
+  }, `Setting filter to '${filter}' should taint the canvas.`);
+
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.beginLayer({filter});
+    ctx.fillRect(0, 0, 300, 150);
+    ctx.endLayer();
+    assert_throws_dom("SecurityError", () => ctx.getImageData(1, 1, 1, 1));
+  }, `Using a layer with filter '${filter}' should taint the canvas.`);
+}
+</script>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/canvas-filter-url-reference-tainting.html b/third_party/blink/web_tests/wpt_internal/html/canvas/canvas-filter-url-reference-tainting.html
new file mode 100644
index 0000000..50b2a8c2
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/canvas-filter-url-reference-tainting.html
@@ -0,0 +1,84 @@
+<!doctype html>
+<title>CSS url() filter passed to beginLayer may taint the canvas</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script src="/common/get-host-info.sub.js"></script>
+
+<svg style="display: block; width: 0; height: 0">
+  <defs>
+    <!-- Not tainted. -->
+    <filter id="floodLime">
+      <feFlood flood-color="lime"/>
+    </filter>
+    <filter id="sameOriginImage">
+      <feImage href="/images/green.svg"/>
+    </filter>
+
+    <!-- Tainted. -->
+    <filter id="floodCurrentColor">
+      <feFlood flood-color="currentColor"/>
+    </filter>
+    <filter id="elementImage">
+      <feImage href="#circle"/>
+    </filter>
+    <filter id="crossOriginImage">
+      <feImage>
+        <script>
+          document.currentScript.parentElement.setAttribute(
+              'href', `${get_host_info().REMOTE_ORIGIN}/images/green.svg`);
+        </script>
+      </feImage>
+    </filter>
+    <circle id="circle" r="100" fill="lime"/>
+  </defs>
+</svg>
+
+<script type="module">
+await new Promise(resolve => window.addEventListener('load', resolve));
+
+// SVG reference filters taint the canvas according to
+// https://drafts.csswg.org/filter-effects/#tainted-filter-primitives
+
+// Not tainted.
+for (let filter of [
+  `url(${location.href}#floodLime)`,
+  `url(${location.href}#sameOriginImage)`,
+]) {
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.filter = filter;
+    ctx.fillRect(0, 0, 300, 150);
+    assert_array_equals(ctx.getImageData(1, 1, 1, 1).data, [0, 255, 0, 255]);
+  }, `Setting filter to '${filter}' should not taint the canvas.`);
+
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.beginLayer({filter});
+    ctx.fillRect(0, 0, 300, 150);
+    ctx.endLayer();
+    assert_array_equals(ctx.getImageData(1, 1, 1, 1).data, [0, 255, 0, 255]);
+  }, `Using a layer with filter '${filter}' should not taint the canvas.`);
+}
+
+// Tainted.
+for (let filter of [
+  `url(${location.href}#floodCurrentColor)`,
+  `url(${location.href}#elementImage)`,
+  `url(${location.href}#crossOriginImage)`,
+]) {
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.filter = filter;
+    ctx.fillRect(0, 0, 300, 150);
+    assert_throws_dom("SecurityError", () => ctx.getImageData(1, 1, 1, 1));
+  }, `Setting filter to '${filter}' should taint the canvas.`);
+
+  test(() => {
+    const ctx = document.createElement('canvas').getContext('2d');
+    ctx.beginLayer({filter});
+    ctx.fillRect(0, 0, 300, 150);
+    ctx.endLayer();
+    assert_throws_dom("SecurityError", () => ctx.getImageData(1, 1, 1, 1));
+  }, `Using a layer with filter '${filter}' should taint the canvas.`);
+}
+</script>
Loading diff…

Original Bug Report

reported by [email protected]

SOP Bypass via missing OriginTainted check in Canvas2D beginLayer filter

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team.

Overview: The experimental Canvas2D beginLayer API applies filters but fails to check if the resulting filter graph is origin-tainted. An attacker can use an SVG <feImage> filter to render a cross-origin image onto the canvas, which fails to taint the canvas origin and allows the pixels to be extracted via getImageData().

Affected files:

  • third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc

Estimated timestamp from git blame: 2024-08-22

Summary

A potential Same-Origin Policy (SOP) bypass exists in the experimental beginLayer API for Canvas2D. The implementation applies filter effects to the canvas layer but fails to check if those filters contain cross-origin data. This allows an attacker to read cross-origin image data via getImageData(), bypassing the Same-Origin Policy.

Technical Description

In third_party/blink/renderer/modules/canvas/canvas2d/canvas_2d_recorder_context.cc, the beginLayerImpl function resolves a filter option into a FilterEffect graph. This graph is immediately converted to a cc::PaintFilter and applied to the layer. However, the code fails to check if the resulting filter graph is origin-tainted.

// canvas_2d_recorder_context.cc : Canvas2DRecorderContext::beginLayerImpl
479:       filter = paint_filter_builder::Build(
480:           filter_effect_builder.BuildFilterEffect(std::move(filter_operations),
481:                                                   !OriginClean()),
482:           kInterpolationSpaceSRGB);

The FilterEffect* returned by BuildFilterEffect contains an OriginTainted() flag. For comparison, the standard ctx.filter implementation in CanvasRenderingContext2DState::GetFilter() correctly checks this flag and taints the canvas:

// canvas_rendering_context_2d_state.cc : CanvasRenderingContext2DState::GetFilter
624:       if (last_effect->OriginTainted())
625:         context->SetOriginTainted();

Because beginLayerImpl neglects to call SetOriginTainted() on the context, the canvas’s OriginClean flag remains true.

An attacker can exploit this by passing an SVG filter (url(#f)) to beginLayer. If the SVG filter uses an <feImage> element pointing to a cross-origin URL, the image is fetched in kNoCors mode. SVGFEImageElement::TaintsOrigin() correctly identifies this and taints the internal FilterEffect graph. However, because beginLayerImpl ignores the tainted status, the cross-origin pixels are rasterized into the canvas backing store when endLayer() is called, while the canvas remains “clean”. The attacker can then use getImageData(), toDataURL(), or toBlob() to extract the sensitive cross-origin pixel data.

Potential Steps to Reproduce

Note: Our tooling agent does not have the ability to run code, but the following steps are highly likely to reproduce the issue.

  1. Launch Chrome with --enable-experimental-web-platform-features or enable the #canvas-2d-layers flag.
  2. Host a page with the following HTML/JS on an attacker-controlled origin:
<svg width="0" height="0">
  <filter id="f" x="0" y="0" width="100%" height="100%">
    <feImage href="https://victim.example/authenticated-image.png" width="300" height="150"/>
  </filter>
</svg>
<canvas id="c" width="300" height="150"></canvas>
<script>
  const c = document.getElementById('c');
  const ctx = c.getContext('2d');
  
  // Wait for the cross-origin image to load, then:
  setTimeout(() => {
    ctx.beginLayer({filter: 'url(#f)'});
    ctx.fillRect(0, 0, 1, 1); // Triggers layer application
    ctx.endLayer();
    
    try {
      const data = ctx.getImageData(0, 0, c.width, c.height);
      console.log("Pixels extracted successfully:", data.data);
    } catch (e) {
      console.log("Caught SecurityError:", e);
    }
  }, 1000);
</script>

Suggested Fix

Update Canvas2DRecorderContext::beginLayerImpl() to capture the FilterEffect* returned by BuildFilterEffect. Check its OriginTainted() status, and call SetOriginTainted() on the context if the filter is tainted:

FilterEffect* filter_effect = filter_effect_builder.BuildFilterEffect(
    std::move(filter_operations), !OriginClean());
if (filter_effect && filter_effect->OriginTainted()) {
  SetOriginTainted();
}
filter = paint_filter_builder::Build(filter_effect, kInterpolationSpaceSRGB);

Evaluated with Chrome root at commit: 096fc8fdbfacf2546485756d03f160a3d04fcc9b


Results 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.

View on issue tracker