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 <[email protected]> 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 <[email protected]> Commit-Queue: Philip Rogers <[email protected]> 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