Medium watch CVSS 6.5 chrome Info Disclosure 🔧 Commit mapped

Overview

Medium
Severity
6.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Paint
DescriptionInformation leak in Paint
ComponentPaint
Sub-componentHiC
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.
Tracker553136980
Fix commitf32b64bb9703 (chromium/src) +96/-0
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-08-26 (per vendor advisory)
Fix landed2026-09-11 (public git)
Disclosed2026-09-17
Patch gap6 days — fix public in source before the advisory

Background

HTML-in-Canvas (HiC) / `drawElementImage`
an experimental feature that renders a live DOM element (here an <svg> inside a <canvas layoutsubtree>) into the 2D canvas bitmap, which script can then read back with getImageData.
Privacy-preserving paint (`PaintInfo::IsPrivacyPreserving`)
a paint mode used when snapshotting content into a canvas, in which privacy-sensitive rendering such as :visited link styling and form-control autofill must be suppressed because the resulting pixels are script-readable.
SVG marker (`LayoutSVGResourceMarker`)
a reusable SVG resource (referenced via marker-start/marker-mid/marker-end) that can contain arbitrary drawable content, including images, filters, and <foreignObject> HTML.
`Element::IsInCanvasSubtree`
a predicate that returns whether a DOM element lives inside a canvas’s HiC layout subtree, i.e. within the content region that the privacy-preserving snapshot actually covers.

Root Cause Analysis

When an SVG shape is painted during a privacy-preserving canvas snapshot, svg_shape_painter.cc resolves its marker-start, marker-mid, and marker-end references into LayoutSVGResourceMarker objects via GetSVGResourceAsType and paints them. Before the patch this resolution honored any marker the shape referenced, including markers defined by <marker> elements located outside the canvas subtree — content that is not part of, and not governed by, the privacy-preserving snapshot. Because a marker can embed arbitrary content (images, filters, or a <foreignObject> containing HTML whose appearance depends on :visited state or autofill), painting an out-of-subtree marker draws privacy-sensitive pixels into a readback-accessible canvas, breaking the invariant that only privacy-protected in-subtree content is rendered in this mode.

The fix adds a guard, active only when paint_info.IsPrivacyPreserving(), that nulls out any of the three markers whose defining GetElement() returns false for IsInCanvasSubtree(). This runs before the existing if (!marker_start && !marker_mid && !marker_end) return; short-circuit and before any downstream marker painting, so disallowed external markers are simply never drawn. It mirrors the protection already applied to SVG patterns.

Key insight
The core mistake was applying markers by reference alone without checking that the referenced marker element lives inside the privacy-preserving canvas subtree, letting unprotected external content leak into a script-readable bitmap; the fix scopes marker usage to in-subtree markers whenever the paint is privacy-preserving.

Attack Path

  1. Set up a HiC canvas The attacker page places a <canvas layoutsubtree> containing an <svg> child and calls ctx.drawElementImage(child, ...), triggering a privacy-preserving paint of the SVG subtree into the canvas.
  2. Reference an external marker A <path> inside the in-canvas SVG uses marker-start/marker-mid/marker-end pointing at a <marker> defined outside the canvas subtree, whose content (image, filter, or <foreignObject>) is not covered by privacy protections.
  3. Embed privacy-sensitive content The external marker draws content whose pixels depend on privacy-sensitive state such as :visited link color or form-control autofill (inferred, as the concrete leak vector; the patch and comment name these as the protected classes).
  4. Read back the pixels After the marker paints into the canvas, the attacker calls getImageData and inspects pixel values (as the shipped test does at fixed coordinates) to recover the privacy-sensitive state.

Impact Assessment

An attacker gains an information leak in the renderer process: the ability to observe privacy-sensitive rendering (e.g. visited-link status or autofilled form values) that should never be script-observable, by smuggling it through an out-of-subtree SVG marker painted into a readback-accessible canvas. The precondition is that the HTML-in-Canvas / drawElementImage feature is available and the attacker can script a canvas snapshot on their own page. The vendor advisory rates this medium severity and does not report in-the-wild exploitation; NVD scores it CVSS 6.5. Any escalation beyond pixel-level privacy disclosure is not demonstrated by the patch.

Changed Functions

FunctionChangeNotes
promise_test
third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-marker-outside-subtree-ignored.tentative.html
modified

Files Changed

  • third_party/blink/renderer/core/paint/svg_shape_painter.cc
  • third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-marker-outside-subtree-ignored.tentative.html

Audit Directions

  • Externally-referenced SVG resources during privacy-preserving paint
    Audit every SVG resource type resolved by reference (patterns, masks, clip-paths, filters, gradients, paint-order, mask, use) for the same missing IsInCanvasSubtree/privacy check when IsPrivacyPreserving() is set.
  • Canvas-subtree boundary enforcement
    Look for paint or layout code that follows resource IDs or url(#...) references across the canvas-subtree boundary without confirming the target element is in-subtree, since such crossings can pull unprotected content into readback-accessible bitmaps.
  • New privacy-preserving sinks
    Wherever PaintInfo::IsPrivacyPreserving() gates behavior, verify the guard runs before every content-emitting path (not just the primary one) and that newly added paint features (like HiC) opt into it, since the check is easy to omit on secondary code paths.
From f32b64bb9703eccac76a8a6c891c90841eaf2ec2 Mon Sep 17 00:00:00 2001
From: Stephen Chenney <[email protected]>
Date: Thu, 10 Sep 2026 18:11:45 -0700
Subject: [PATCH] [HiC] Disallow SVG markers that are outside a canvas subtree

SVG Markers may contain arbitrary HTML content through a
<foreignObject>, not to mention a wide variety of images
or filters. As with SVG patterns, we disallow the use of
markers defined outside of a canvas subtree, where privacy
protections are not available, particularly for visited
links and form control autofill.

Fixed: 553136980
Change-Id: I51dd1ab951949b79824ac00ae27dc134c3bc12c9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8385141
Reviewed-by: Stefan Zager <[email protected]>
Commit-Queue: Stephen Chenney <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1695874}
---
diff --git a/third_party/blink/renderer/core/paint/svg_shape_painter.cc b/third_party/blink/renderer/core/paint/svg_shape_painter.cc
index ea10cbf7..2e963f5 100644
--- a/third_party/blink/renderer/core/paint/svg_shape_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_shape_painter.cc
@@ -266,6 +266,20 @@
       *client, style.MarkerMidResource());
   auto* marker_end = GetSVGResourceAsType<LayoutSVGResourceMarker>(
       *client, style.MarkerEndResource());
+  if (paint_info.IsPrivacyPreserving()) {
+    if (marker_start && marker_start->GetElement() &&
+        !marker_start->GetElement()->IsInCanvasSubtree()) {
+      marker_start = nullptr;
+    }
+    if (marker_mid && marker_mid->GetElement() &&
+        !marker_mid->GetElement()->IsInCanvasSubtree()) {
+      marker_mid = nullptr;
+    }
+    if (marker_end && marker_end->GetElement() &&
+        !marker_end->GetElement()->IsInCanvasSubtree()) {
+      marker_end = nullptr;
+    }
+  }
   if (!marker_start && !marker_mid && !marker_end)
     return;
 
diff --git a/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-marker-outside-subtree-ignored.tentative.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-marker-outside-subtree-ignored.tentative.html
new file mode 100644
index 0000000..23111f15
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-marker-outside-subtree-ignored.tentative.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not use SVG markers outside the subtree</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>
+      <marker id="markerOut" markerUnits="userSpaceOnUse" viewBox="0 0 100 100" markerWidth="20" markerHeight="20" refX="50" refY="50">
+        <image href="/images/red-100x100.png" x="0" y="0" width="100" height="100" preserveAspectRatio="none"/>
+      </marker>
+    </defs>
+  </svg>
+
+  <canvas id="canvas" width="100" height="100" layoutsubtree>
+    <svg id=child width="100" height="100">
+      <marker id="markerIn" markerUnits="userSpaceOnUse" viewBox="0 0 100 100" markerWidth="20" markerHeight="20" refX="50" refY="50">
+        <image href="/images/green-100x100.png" x="0" y="0" width="100" height="100" preserveAspectRatio="none"/>
+      </marker>
+      <path d="M 20 20 L 50 20 L 80 20"
+        stroke="black"
+        marker-start="url(#markerOut)"
+        marker-mid="url(#markerOut)"
+        marker-end="url(#markerOut)" />
+      <path d="M 20 40 L 50 40 L 80 40"
+        stroke="black"
+        marker-start="url(#markerIn)"
+        marker-mid="url(#markerIn)"
+        marker-end="url(#markerIn)" />
+    </svg>
+  </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 imageEl1 = document.querySelector('#markerOut image');
+        const imageEl2 = document.querySelector('#markerIn image');
+        await Promise.all([
+          preloadImage(imageEl1.getAttribute('href')),
+          preloadImage(imageEl2.getAttribute('href')),
+        ]);
+
+        await waitForCanvasPaint(canvas);
+        var ctx = canvas.getContext('2d');
+        ctx.fillStyle = 'white';
+        ctx.fillRect(0, 0, 100, 100);
+        ctx.drawElementImage(child, 0, 0);
+
+        // The canvas should ignore the marker and draw the line black.
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+        let pixel = _getPixelFromImageData(imgData, 15, 25);
+        assert_array_equals(pixel, [255, 255, 255, 255], "External start marker should not draw");
+        pixel = _getPixelFromImageData(imgData, 45, 25);
+        assert_array_equals(pixel, [255, 255, 255, 255], "External mid marker should not draw");
+        pixel = _getPixelFromImageData(imgData, 85, 25);
+        assert_array_equals(pixel, [255, 255, 255, 255], "External end marker should not draw");
+
+        pixel = _getPixelFromImageData(imgData, 15, 45);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Internal start marker should draw");
+        pixel = _getPixelFromImageData(imgData, 45, 45);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Internal mid marker should draw");
+        pixel = _getPixelFromImageData(imgData, 85, 45);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Internal end marker should draw");
+      });
+    }
+  </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-marker-outside-subtree-ignored.tentative.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-marker-outside-subtree-ignored.tentative.html
new file mode 100644
index 0000000..23111f15
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-marker-outside-subtree-ignored.tentative.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not use SVG markers outside the subtree</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>
+      <marker id="markerOut" markerUnits="userSpaceOnUse" viewBox="0 0 100 100" markerWidth="20" markerHeight="20" refX="50" refY="50">
+        <image href="/images/red-100x100.png" x="0" y="0" width="100" height="100" preserveAspectRatio="none"/>
+      </marker>
+    </defs>
+  </svg>
+
+  <canvas id="canvas" width="100" height="100" layoutsubtree>
+    <svg id=child width="100" height="100">
+      <marker id="markerIn" markerUnits="userSpaceOnUse" viewBox="0 0 100 100" markerWidth="20" markerHeight="20" refX="50" refY="50">
+        <image href="/images/green-100x100.png" x="0" y="0" width="100" height="100" preserveAspectRatio="none"/>
+      </marker>
+      <path d="M 20 20 L 50 20 L 80 20"
+        stroke="black"
+        marker-start="url(#markerOut)"
+        marker-mid="url(#markerOut)"
+        marker-end="url(#markerOut)" />
+      <path d="M 20 40 L 50 40 L 80 40"
+        stroke="black"
+        marker-start="url(#markerIn)"
+        marker-mid="url(#markerIn)"
+        marker-end="url(#markerIn)" />
+    </svg>
+  </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 imageEl1 = document.querySelector('#markerOut image');
+        const imageEl2 = document.querySelector('#markerIn image');
+        await Promise.all([
+          preloadImage(imageEl1.getAttribute('href')),
+          preloadImage(imageEl2.getAttribute('href')),
+        ]);
+
+        await waitForCanvasPaint(canvas);
+        var ctx = canvas.getContext('2d');
+        ctx.fillStyle = 'white';
+        ctx.fillRect(0, 0, 100, 100);
+        ctx.drawElementImage(child, 0, 0);
+
+        // The canvas should ignore the marker and draw the line black.
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+        let pixel = _getPixelFromImageData(imgData, 15, 25);
+        assert_array_equals(pixel, [255, 255, 255, 255], "External start marker should not draw");
+        pixel = _getPixelFromImageData(imgData, 45, 25);
+        assert_array_equals(pixel, [255, 255, 255, 255], "External mid marker should not draw");
+        pixel = _getPixelFromImageData(imgData, 85, 25);
+        assert_array_equals(pixel, [255, 255, 255, 255], "External end marker should not draw");
+
+        pixel = _getPixelFromImageData(imgData, 15, 45);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Internal start marker should draw");
+        pixel = _getPixelFromImageData(imgData, 45, 45);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Internal mid marker should draw");
+        pixel = _getPixelFromImageData(imgData, 85, 45);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Internal end marker should 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.