Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactPolicy bypass in Paint
DescriptionPolicy bypass in Paint
ComponentPaint
Bug ClassLogic Error
Tracker501597365
Fix commit38e22bf93eb3 (chromium/src) +110/-17
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc
modified
if
third_party/blink/renderer/core/paint/svg_object_painter.cc
modified

Files Changed

  • third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.cc
  • third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.h
  • third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h
  • third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc
  • third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.h
  • third_party/blink/renderer/core/paint/svg_object_painter.cc
From 38e22bf93eb317b9aec320701f0f401ee760c2fb Mon Sep 17 00:00:00 2001
From: Philip Rogers <pdr@chromium.org>
Date: Mon, 27 Apr 2026 07:16:58 -0700
Subject: [PATCH] [html-in-canvas] Do not paint cross-origin SVG patterns

This patch plumbs PaintFlags through SVG resource painting to ensure
cross-origin images within SVG patterns are blocked when drawing with
privacy-preserving painting.

Fixed: 501597365
Change-Id: I8bd52f1f64a074ca6bcb8031a1e9948c42d7517c
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7794467
Reviewed-by: Stephen Chenney <schenney@chromium.org>
Auto-Submit: Philip Rogers <pdr@chromium.org>
Commit-Queue: Stephen Chenney <schenney@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1621069}
---

diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.cc b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.cc
index 18ca8c15..028d430 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.cc
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.cc
@@ -147,7 +147,8 @@
     const gfx::RectF& reference_box,
     const AffineTransform* additional_transform,
     const AutoDarkMode& auto_dark_mode,
-    cc::PaintFlags& flags) {
+    cc::PaintFlags& flags,
+    PaintFlags /* paint_flags */) {
   NOT_DESTROYED();
   ClearInvalidationMask();
 
diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.h b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.h
index dfc7cd5..d154d1a 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.h
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.h
@@ -47,7 +47,8 @@
                    const gfx::RectF& reference_box,
                    const AffineTransform* additional_transform,
                    const AutoDarkMode& auto_dark_mode,
-                   cc::PaintFlags&) final;
+                   cc::PaintFlags& flags,
+                   PaintFlags paint_flags) final;
 
   bool IsChildAllowed(LayoutObject* child, const ComputedStyle&) const final;
 
diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h
index 069de534..ba84b0b 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.h
@@ -22,6 +22,7 @@
 
 #include "cc/paint/paint_flags.h"
 #include "third_party/blink/renderer/core/layout/svg/layout_svg_resource_container.h"
+#include "third_party/blink/renderer/core/paint/paint_flags.h"
 
 namespace blink {
 
@@ -36,7 +37,8 @@
                            const gfx::RectF& reference_box,
                            const AffineTransform* additional_transform,
                            const AutoDarkMode& auto_dark_mode,
-                           cc::PaintFlags&) = 0;
+                           cc::PaintFlags&,
+                           PaintFlags paint_flags) = 0;
 
  protected:
   void StyleDidChange(StyleDifference,
diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc
index 90c2de36..85e5ccb1 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc
@@ -47,6 +47,7 @@
  public:
   std::unique_ptr<Pattern> pattern;
   AffineTransform transform;
+  PaintFlags paint_flags = PaintFlag::kNoFlag;
 };
 
 LayoutSVGResourcePattern::LayoutSVGResourcePattern(SVGPatternElement* node)
@@ -123,7 +124,8 @@
 }
 
 std::unique_ptr<PatternData> LayoutSVGResourcePattern::BuildPatternData(
-    const gfx::RectF& object_bounding_box) {
+    const gfx::RectF& object_bounding_box,
+    PaintFlags paint_flags) {
   NOT_DESTROYED();
   auto pattern_data = std::make_unique<PatternData>();
 
@@ -170,7 +172,8 @@
   }
 
   pattern_data->pattern = Pattern::CreatePaintRecordPattern(
-      AsPaintRecord(tile_transform), gfx::RectF(tile_bounds.size()));
+      AsPaintRecord(tile_transform, paint_flags),
+      gfx::RectF(tile_bounds.size()));
 
   // Compute pattern space transformation.
   pattern_data->transform.Translate(tile_bounds.x(), tile_bounds.y());
@@ -184,14 +187,21 @@
     const gfx::RectF& reference_box,
     const AffineTransform* additional_transform,
     const AutoDarkMode&,
-    cc::PaintFlags& flags) {
+    cc::PaintFlags& flags,
+    PaintFlags paint_flags) {
   NOT_DESTROYED();
   ClearInvalidationMask();
 
   std::unique_ptr<PatternData>& pattern_data =
       pattern_map_.insert(&client, nullptr).stored_value->value;
-  if (!pattern_data)
-    pattern_data = BuildPatternData(reference_box);
+  if (pattern_data && pattern_data->paint_flags != paint_flags) {
+    pattern_data.reset();
+  }
+
+  if (!pattern_data) {
+    pattern_data = BuildPatternData(reference_box, paint_flags);
+    pattern_data->paint_flags = paint_flags;
+  }
 
   if (!pattern_data->pattern)
     return false;
@@ -205,7 +215,8 @@
 }
 
 PaintRecord LayoutSVGResourcePattern::AsPaintRecord(
-    const AffineTransform& tile_transform) const {
+    const AffineTransform& tile_transform,
+    PaintFlags paint_flags) const {
   NOT_DESTROYED();
   DCHECK(!should_collect_pattern_attributes_);
 
@@ -231,7 +242,8 @@
   PaintRecordBuilder builder;
   for (LayoutObject* child = pattern_layout_object->FirstChild(); child;
        child = child->NextSibling()) {
-    SVGObjectPainter(*child, nullptr).PaintResourceSubtree(builder.Context());
+    SVGObjectPainter(*child, nullptr)
+        .PaintResourceSubtree(builder.Context(), paint_flags);
   }
   canvas->save();
   canvas->concat(tile_transform.ToSkM44());
diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.h b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.h
index 058a7e1..fc73f1c 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.h
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.h
@@ -50,8 +50,9 @@
   bool ApplyShader(const SVGResourceClient&,
                    const gfx::RectF& reference_box,
                    const AffineTransform* additional_transform,
-                   const AutoDarkMode&,
-                   cc::PaintFlags&) override;
+                   const AutoDarkMode& auto_dark_mode,
+                   cc::PaintFlags& flags,
+                   PaintFlags paint_flags) override;
 
   static const LayoutSVGResourceType kResourceType = kPatternResourceType;
   LayoutSVGResourceType ResourceType() const override {
@@ -67,8 +68,10 @@
 
   bool FindCycleFromSelf() const override;
   std::unique_ptr<PatternData> BuildPatternData(
-      const gfx::RectF& object_bounding_box);
-  PaintRecord AsPaintRecord(const AffineTransform&) const;
+      const gfx::RectF& object_bounding_box,
+      PaintFlags paint_flags);
+  PaintRecord AsPaintRecord(const AffineTransform&,
+                            PaintFlags paint_flags) const;
 
   mutable bool should_collect_pattern_attributes_ : 1;
   mutable PatternAttributes attributes_;
diff --git a/third_party/blink/renderer/core/paint/svg_object_painter.cc b/third_party/blink/renderer/core/paint/svg_object_painter.cc
index 972d79a..35863f4 100644
--- a/third_party/blink/renderer/core/paint/svg_object_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_object_painter.cc
@@ -18,7 +18,8 @@
 bool ApplyPaintResource(
     const SvgContextPaints::ContextPaint& context_paint,
     const AffineTransform* additional_paint_server_transform,
-    cc::PaintFlags& flags) {
+    cc::PaintFlags& flags,
+    PaintFlags paint_flags) {
   SVGElementResourceClient* client =
       SVGResources::GetClient(context_paint.object);
   if (!client) {
@@ -34,7 +35,8 @@
       context_paint.object.StyleRef(), DarkModeFilter::ElementRole::kSVG));
   if (!uri_resource->ApplyShader(
           *client, SVGResources::ReferenceBoxForEffects(context_paint.object),
-          additional_paint_server_transform, auto_dark_mode, flags)) {
+          additional_paint_server_transform, auto_dark_mode, flags,
+          paint_flags)) {
     return false;
   }
   return true;
@@ -151,7 +153,8 @@
     std::optional<AffineTransform> resolved_transform = ResolveContextTransform(
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/svg-pattern-cross-origin-image.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/svg-pattern-cross-origin-image.https.sub.html
new file mode 100644
index 0000000..074cce88
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/svg-pattern-cross-origin-image.https.sub.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+  <title>drawElementImage does not use cross-origin images in SVG patterns</title>
+  <link rel="help" href="https://github.com/WICG/html-in-canvas">
+  <script src='/resources/testharness.js'></script>
+  <script src='/resources/testharnessreport.js'></script>
+</head>
+<body>
+  <canvas id="canvas" width="100" height="100" layoutsubtree>
+    <svg width="100" height="100">
+      <defs>
+        <pattern id="pattern-same" patternUnits="userSpaceOnUse" x="0" y="0" width="100" height="50">
+          <image href="https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/green-100x100.png" x="0" y="0" width="100" height="50" preserveAspectRatio="none"/>
+        </pattern>
+        <pattern id="pattern-cross" patternUnits="userSpaceOnUse" x="0" y="0" width="100" height="50">
+          <image href="https://{{hosts[alt][www]}}:{{ports[h2][0]}}/wpt_internal/html/canvas/drawElementImage/resources/red-100x100.png" x="0" y="0" width="100" height="50" preserveAspectRatio="none"/>
+        </pattern>
+      </defs>
+      <rect x="0" y="0" width="100" height="50" fill="url(#pattern-same)"/>
+      <rect x="0" y="50" width="100" height="50" fill="url(#pattern-cross)"/>
+    </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 sameOriginImageEl = document.querySelector('#pattern-same image');
+        const crossOriginImageEl = document.querySelector('#pattern-cross image');
+        await Promise.all([
+          preloadImage(sameOriginImageEl.getAttribute('href')),
+          preloadImage(crossOriginImageEl.getAttribute('href'))
+        ]);
+
+        await new Promise(requestAnimationFrame);
+        await new Promise(setTimeout);
+        var ctx = canvas.getContext('2d');
+        ctx.fillStyle = 'blue';
+        ctx.fillRect(0, 0, 100, 100);
+        ctx.drawElementImage(document.querySelector('svg'), 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+
+        // Helper function to extract a pixel's RGBA array at (x, y).
+        const getPixel = (x, y) => {
+          const index = (y * canvas.width + x) * 4;
+          return [
+            imgData[index],
+            imgData[index + 1],
+            imgData[index + 2],
+            imgData[index + 3]
+          ];
+        };
+
+        let pixel = getPixel(50, 25);
+        assert_array_equals(pixel, [0, 255, 0, 255], "Same origin pattern should draw");
+        pixel = getPixel(50, 75);
+        assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin pattern should not 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.