Chrome · Paint
CVE-2026-11132
Logic Error in Paint
Overview
Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.cc |
modified | |
ifthird_party/blink/renderer/core/paint/svg_object_painter.cc |
modified |
Files Changed
third_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.ccthird_party/blink/renderer/core/layout/svg/layout_svg_resource_gradient.hthird_party/blink/renderer/core/layout/svg/layout_svg_resource_paint_server.hthird_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.ccthird_party/blink/renderer/core/layout/svg/layout_svg_resource_pattern.hthird_party/blink/renderer/core/paint/svg_object_painter.cc
Patch
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.
References
On This Page