Chrome · SVG
CVE-2026-13793
Logic Error in SVG
Overview
High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.cc |
modified | |
ifthird_party/blink/renderer/core/paint/paint_layer_painter.cc |
modified | |
ifthird_party/blink/renderer/core/paint/scoped_svg_paint_state.cc |
modified | |
ifthird_party/blink/renderer/core/paint/svg_mask_painter.cc |
modified |
Files Changed
third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.ccthird_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.hthird_party/blink/renderer/core/paint/box_painter_base.ccthird_party/blink/renderer/core/paint/paint_layer_painter.ccthird_party/blink/renderer/core/paint/scoped_svg_paint_state.ccthird_party/blink/renderer/core/paint/svg_mask_painter.ccthird_party/blink/renderer/core/paint/svg_mask_painter.h
Patch
From 4855238b10f2054c2d258cd8144237f2d93bd634 Mon Sep 17 00:00:00 2001
From: Philip Rogers <pdr@chromium.org>
Date: Wed, 13 May 2026 14:48:37 -0700
Subject: [PATCH] [html-in-canvas] Fix cross-origin image leak in SVG masks
This CL fixes a privacy leak where cross-origin images used inside
<mask> elements were incorrectly rendered into <canvas>.
Fixed: 510829679
Bug: 512516331
Change-Id: Ibaed202157c13fcf42cb01b71fa92140d872a1ed
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7841426
Reviewed-by: Stefan Zager <szager@chromium.org>
Commit-Queue: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1630234}
---
diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.cc b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.cc
index d3a8014..4394fcb 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.cc
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.cc
@@ -44,8 +44,12 @@
kPaintInvalidation);
}
-PaintRecord LayoutSVGResourceMasker::CreatePaintRecord() {
+PaintRecord LayoutSVGResourceMasker::CreatePaintRecord(PaintFlags paint_flags) {
NOT_DESTROYED();
+ if (cached_paint_record_ && cached_paint_flags_ != paint_flags) {
+ cached_paint_record_ = std::nullopt;
+ }
+
if (cached_paint_record_)
return *cached_paint_record_;
@@ -59,10 +63,12 @@
layout_object->StyleRef().Display() == EDisplay::kNone)
continue;
SVGObjectPainter(*layout_object, nullptr)
- .PaintResourceSubtree(builder.Context(), PaintFlag::kPaintingSVGMask);
+ .PaintResourceSubtree(builder.Context(),
+ PaintFlag::kPaintingSVGMask | paint_flags);
}
cached_paint_record_ = builder.EndRecording();
+ cached_paint_flags_ = paint_flags;
return *cached_paint_record_;
}
diff --git a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.h b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.h
index e100362..bc2a07bb 100644
--- a/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.h
+++ b/third_party/blink/renderer/core/layout/svg/layout_svg_resource_masker.h
@@ -23,6 +23,7 @@
#include <optional>
#include "third_party/blink/renderer/core/layout/svg/layout_svg_resource_container.h"
+#include "third_party/blink/renderer/core/paint/paint_flags.h"
#include "third_party/blink/renderer/core/svg/svg_unit_types.h"
#include "ui/gfx/geometry/rect_f.h"
@@ -54,10 +55,11 @@
return kResourceType;
}
- PaintRecord CreatePaintRecord();
+ PaintRecord CreatePaintRecord(PaintFlags paint_flags);
private:
std::optional<PaintRecord> cached_paint_record_;
+ PaintFlags cached_paint_flags_ = PaintFlag::kNoFlag;
};
template <>
diff --git a/third_party/blink/renderer/core/paint/box_painter_base.cc b/third_party/blink/renderer/core/paint/box_painter_base.cc
index 1bd91a17..2802e009 100644
--- a/third_party/blink/renderer/core/paint/box_painter_base.cc
+++ b/third_party/blink/renderer/core/paint/box_painter_base.cc
@@ -1421,7 +1421,8 @@
SVGMaskPainter::PaintSVGMaskLayer(
context, *mask_source, bg_paint_context.ImageClient(),
reference_box, zoom, composite_op,
- bg_layer.MaskMode() == EFillMaskMode::kMatchSource);
+ bg_layer.MaskMode() == EFillMaskMode::kMatchSource,
+ paint_info.GetPaintFlags());
return;
}
}
diff --git a/third_party/blink/renderer/core/paint/paint_layer_painter.cc b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
index eb1c45b..afd6f1e 100644
--- a/third_party/blink/renderer/core/paint/paint_layer_painter.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer_painter.cc
@@ -503,7 +503,7 @@
if (should_paint_content && !selection_drag_image_only) {
if (properties->Mask()) {
if (object.IsSVGForeignObject()) {
- SVGMaskPainter::Paint(context, object, object);
+ SVGMaskPainter::Paint(context, object, object, paint_flags);
} else {
PaintWithPhase(PaintPhase::kMask, context, paint_flags);
}
diff --git a/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc b/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
index 1a25ea57..7d92df1 100644
--- a/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
+++ b/third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
@@ -87,8 +87,10 @@
// the node hierarchy), to ensure the clip-path mask will be applied to the
// mask to create an intersection of the masks, then the intersection will be
// applied to the masked content.
- if (should_paint_mask_)
- SVGMaskPainter::Paint(paint_info_.context, object_, display_item_client_);
+ if (should_paint_mask_) {
+ SVGMaskPainter::Paint(paint_info_.context, object_, display_item_client_,
+ paint_info_.GetPaintFlags());
+ }
if (should_paint_clip_path_as_mask_image_) {
ClipPathClipper::PaintClipPathAsMaskImage(paint_info_.context, object_,
diff --git a/third_party/blink/renderer/core/paint/svg_mask_painter.cc b/third_party/blink/renderer/core/paint/svg_mask_painter.cc
index 23c6c88..0f136f8 100644
--- a/third_party/blink/renderer/core/paint/svg_mask_painter.cc
+++ b/third_party/blink/renderer/core/paint/svg_mask_painter.cc
@@ -95,7 +95,8 @@
void PaintMaskLayer(const FillLayer& layer,
const LayoutObject& object,
const SVGBackgroundPaintContext& bg_paint_context,
- GraphicsContext& context) {
+ GraphicsContext& context,
+ PaintFlags paint_flags) {
const StyleImage* style_image = layer.GetImage();
if (!style_image) {
return;
@@ -131,7 +132,7 @@
saver.Save();
SVGMaskPainter::PaintSVGMaskLayer(
context, *mask_source, observer, reference_box, zoom, composite_op,
- layer.MaskMode() == EFillMaskMode::kMatchSource);
+ layer.MaskMode() == EFillMaskMode::kMatchSource, paint_flags);
return;
}
@@ -237,7 +238,8 @@
void SVGMaskPainter::Paint(GraphicsContext& context,
const LayoutObject& layout_object,
- const DisplayItemClient& display_item_client) {
+ const DisplayItemClient& display_item_client,
+ PaintFlags paint_flags) {
const auto* properties = layout_object.FirstFragment().PaintProperties();
DCHECK(properties);
DCHECK(properties->Mask());
@@ -259,11 +261,13 @@
gfx::ToEnclosingRect(visual_rect));
const SVGBackgroundPaintContext bg_paint_context(layout_object);
- IterateFillLayersReveresed(
- &layout_object.StyleRef().MaskLayers(),
- [&layout_object, &bg_paint_context, &context](const FillLayer& layer) {
- PaintMaskLayer(layer, layout_object, bg_paint_context, context);
- });
+ IterateFillLayersReveresed(&layout_object.StyleRef().MaskLayers(),
+ [&layout_object, &bg_paint_context, &context,
+ paint_flags](const FillLayer& layer) {
+ PaintMaskLayer(layer, layout_object,
+ bg_paint_context, context,
+ paint_flags);
+ });
}
void SVGMaskPainter::PaintSVGMaskLayer(GraphicsContext& context,
@@ -272,7 +276,8 @@
const gfx::RectF& reference_box,
const float zoom,
const SkBlendMode composite_op,
- const bool apply_mask_type) {
+ const bool apply_mask_type,
+ PaintFlags paint_flags) {
LayoutSVGResourceMasker* masker =
ResolveElementReference(mask_source, observer);
if (!masker) {
@@ -281,7 +286,7 @@
const AffineTransform content_transformation =
MaskToContentTransform(*masker, reference_box, zoom);
SubtreeContentTransformScope content_transform_scope(content_transformation);
- PaintRecord record = masker->CreatePaintRecord();
+ PaintRecord record = masker->CreatePaintRecord(paint_flags);
context.Clip(masker->ResourceBoundingBox(reference_box, zoom));
diff --git a/third_party/blink/renderer/core/paint/svg_mask_painter.h b/third_party/blink/renderer/core/paint/svg_mask_painter.h
index 9512656..b0647d3 100644
--- a/third_party/blink/renderer/core/paint/svg_mask_painter.h
+++ b/third_party/blink/renderer/core/paint/svg_mask_painter.h
@@ -5,6 +5,7 @@
#ifndef THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_SVG_MASK_PAINTER_H_
#define THIRD_PARTY_BLINK_RENDERER_CORE_PAINT_SVG_MASK_PAINTER_H_
+#include "third_party/blink/renderer/core/paint/paint_flags.h"
#include "third_party/blink/renderer/platform/wtf/allocator/allocator.h"
#include "third_party/skia/include/core/SkBlendMode.h"
Loading diff…
Regression Test / PoC
shipped with the fix
diff --git a/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/svg-mask-image-ignored.https.sub.html b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/svg-mask-image-ignored.https.sub.html
new file mode 100644
index 0000000..eaafd01
--- /dev/null
+++ b/third_party/blink/web_tests/wpt_internal/html/canvas/drawElementImage/privacy/svg-mask-image-ignored.https.sub.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>drawElementImage does not use cross-origin images in SVG masks</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='../support/getPixel.js'></script>
+ <script src="/wpt_internal/resources/canvas-draw-element/waitForCanvasPaint.js"></script>
+</head>
+<body>
+ <canvas id="canvas" width="100" height="200" layoutsubtree>
+ <svg width="100" height="200">
+ <defs>
+ <mask id="mask-same" maskContentUnits="objectBoundingBox">
+ <image href="https://{{location[host]}}/wpt_internal/html/canvas/drawElementImage/resources/blue-100x50-transparent-100x50.png" x="0" y="0" width="1" height="1" preserveAspectRatio="none" />
+ </mask>
+ <mask id="mask-cross" maskContentUnits="objectBoundingBox">
+ <image href="https://{{hosts[alt][www]}}:{{ports[https][0]}}/wpt_internal/html/canvas/drawElementImage/resources/blue-100x50-transparent-100x50.png" x="0" y="0" width="1" height="1" preserveAspectRatio="none" />
+ </mask>
+ </defs>
+ <rect x="0" y="0" width="100" height="100" fill="blue" mask="url(#mask-same)"/>
+ <rect x="0" y="100" width="100" height="100" fill="blue" mask="url(#mask-cross)"/>
+ </svg>
+ </canvas>
+
+ <script>
+ window.onload = () => {
+ promise_test(async function(t) {
+ await waitForCanvasPaint(canvas);
+ var ctx = canvas.getContext('2d');
+ ctx.drawElementImage(document.querySelector('canvas > svg'), 0, 0);
+
+ // Fetch all pixel data once to avoid multiple slow readbacks.
+ const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+ let pixel = getPixel(imgData, 50, 25);
+ assert_array_equals(pixel, [0, 0, 0, 0], "Same origin mask should fully mask top-half of rect");
+
+ pixel = getPixel(imgData, 50, 75);
+ assert_array_equals(pixel, [0, 0, 255, 18], "Same origin mask should not mask bottom-half of rect");
+
+ pixel = getPixel(imgData, 50, 125);
+ assert_array_equals(pixel, [0, 0, 0, 0], "Cross origin mask data should not affect top-half of rect");
+
+ pixel = getPixel(imgData, 50, 175);
+ assert_array_equals(pixel, [0, 0, 0, 0], "Cross origin mask data should not affect bottom-half of rect");
+ });
+ }
+ </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