Medium chrome Logic Error 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOrigin validation error in Paint
DescriptionOrigin validation error in Paint
ComponentPaint
Bug ClassLogic Error
Tracker532952073
Fix commit98b5e8f99f86 (chromium/src) +149/-13
CISA KEVNot listed
CreditedGoogle
Disclosed2026-09-08

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
modified
if
third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
modified
if
third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.cc
modified

Files Changed

  • third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
  • third_party/blink/renderer/core/paint/paint_property_tree_builder.h
  • third_party/blink/renderer/core/paint/scoped_svg_paint_state.cc
  • third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.cc
From 98b5e8f99f864b41632b43e21f113d0a6f482614 Mon Sep 17 00:00:00 2001
From: Stefan Zager <szager@chromium.org>
Date: Thu, 30 Jul 2026 13:52:47 -0700
Subject: [PATCH] [HiC] An origin-tainted filter should taint its subtree

Bug: 532952073
Change-Id: I86a21f9c6fc1928dab4b96f078121818d3409fa0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8131725
Commit-Queue: Stefan Zager <szager@chromium.org>
Reviewed-by: Philip Rogers <pdr@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1671390}
---

diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
index 8165f1e..6e52ca2 100644
--- a/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
+++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder.cc
@@ -2090,6 +2090,7 @@
       }
 
       EffectPaintPropertyNode::State state;
+      state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
       state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
       state.local_transform_space = context_.current.transform;
       if (EffectCanUseCurrentClipAsOutputClip())
@@ -2201,6 +2202,7 @@
 
       if (mask_clip) {
         EffectPaintPropertyNode::State mask_state;
+        mask_state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
         mask_state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
         mask_state.local_transform_space = context_.current.transform;
         mask_state.output_clip = context_.current.clip;
@@ -2225,6 +2227,7 @@
 
       if (needs_mask_based_clip_path_) {
         EffectPaintPropertyNode::State clip_path_state;
+        clip_path_state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
         clip_path_state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
         clip_path_state.local_transform_space = context_.current.transform;
         clip_path_state.output_clip = context_.current.clip;
@@ -2286,6 +2289,7 @@
   CHECK(context_.current.clip);
   CHECK(context_.current.transform);
   EffectPaintPropertyNode::State state;
+  state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
   state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
   state.direct_compositing_reasons = CompositingReason::kElementCapture;
   state.local_transform_space = context_.current.transform;
@@ -2306,6 +2310,7 @@
 
     if (transition) {
       EffectPaintPropertyNode::State state;
+      state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
       state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
       state.local_transform_space = context_.current.transform;
       state.output_clip = context_.current.clip;
@@ -2365,6 +2370,7 @@
       DCHECK(transition);
 
       EffectPaintPropertyNode::State state;
+      state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
       state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
       state.direct_compositing_reasons =
           CompositingReason::kViewTransitionElement;
@@ -2528,21 +2534,26 @@
 
 void FragmentPaintPropertyTreeBuilder::UpdateFilter() {
   DCHECK(properties_);
+  bool was_tainted = properties_->Filter() && properties_->Filter()->Filter() &&
+                     properties_->Filter()->Filter()->OriginTainted();
   if (NeedsPaintPropertyUpdate()) {
     if (NeedsFilter(object_, full_context_)) {
       EffectPaintPropertyNode::State state;
+      state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
       state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
       state.local_transform_space = context_.current.transform;
       EffectPaintPropertyNode::FilterInfo filter_info;
       UpdateFilterEffect(object_, properties_->Filter(), filter_info);
+      bool is_filter_tainted = filter_info.operations.OriginTainted();
       bool is_filter_disallowed =
           RuntimeEnabledFeatures::CanvasDrawElementEnabled(
               object_.GetDocument().GetExecutionContext()) &&
-          object_.IsInCanvasSubtree() && filter_info.operations.OriginTainted();
+          object_.IsInCanvasSubtree() && is_filter_tainted;
       if (!(filter_info.operations.IsEmpty() || is_filter_disallowed)) {
         state.filter_info =
             std::make_unique<EffectPaintPropertyNode::FilterInfo>(
                 std::move(filter_info));
+        state.is_in_tainted_subtree |= is_filter_tainted;
       }
 
       // The CSS filter spec didn't specify how filters interact with overflow
@@ -2603,14 +2614,25 @@
     }
   }
 
+  bool is_tainted = false;
   if (properties_->Filter()) {
     context_.current_effect = properties_->Filter();
     if (const auto* input_clip = properties_->PixelMovingFilterClipExpander()) {
       context_.current.clip = input_clip;
     }
+    if (auto* filter_ops = properties_->Filter()->Filter()) {
+      if (filter_ops->OriginTainted()) {
+        is_tainted = true;
+        context_.is_in_tainted_subtree = true;
+      }
+    }
   } else {
     DCHECK(!properties_->PixelMovingFilterClipExpander());
   }
+  if (was_tainted != is_tainted) {
+    full_context_.force_subtree_update_reasons |=
+        PaintPropertyTreeBuilderContext::kSubtreeUpdateIsolationPiercing;
+  }
 }
 
 static FloatRoundedRect ToSnappedClipRect(const PhysicalRect& rect) {
@@ -3586,6 +3608,7 @@
 
     if (needs_effect_node) {
       EffectPaintPropertyNode::State effect_state;
+      effect_state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
       effect_state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
       effect_state.local_transform_space = context_.current.transform;
       effect_state.output_clip = output_clip;
@@ -3632,6 +3655,7 @@
     // transition, for the same reason as explained above. Scroll corners
     // are only painted for non-overlay scrollbars.
     EffectPaintPropertyNode::State effect_state;
+    effect_state.is_in_tainted_subtree = context_.is_in_tainted_subtree;
     effect_state.is_in_canvas_subtree = context_.is_in_canvas_subtree;
     effect_state.local_transform_space = context_.current.transform;
     effect_state.output_clip = output_clip;
diff --git a/third_party/blink/renderer/core/paint/paint_property_tree_builder.h b/third_party/blink/renderer/core/paint/paint_property_tree_builder.h
index 344715d..5c320785 100644
--- a/third_party/blink/renderer/core/paint/paint_property_tree_builder.h
+++ b/third_party/blink/renderer/core/paint/paint_property_tree_builder.h
@@ -217,6 +217,10 @@
   // all non-alias effects.
   bool self_or_ancestor_participates_in_view_transition = false;
 
+  // Set to true when we visit an object with filter operations that have a
+  // tainted origin, and propagated to all its descendants.
+  bool is_in_tainted_subtree = false;
+
   // Set to true when we visit a canvas child and is propagated to all
   // descendant effects.
   bool is_in_canvas_subtree = false;
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 abbc0cc5..c505001 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
@@ -143,20 +143,22 @@
   auto& paint_controller = paint_info_.context.GetPaintController();
   auto state = paint_controller.CurrentPaintChunkProperties();
 
-  const auto* filter = properties.Filter();
-  if (filter && filter->Filter() && filter->Filter()->OriginTainted() &&
-      (paint_info_.GetPaintFlags() & PaintFlag::kPrivacyPreserving)) {
-    state.SetEffect(*filter->Parent());
-    filter = nullptr;
-  } else if (filter) {
-    state.SetEffect(*filter);
-  } else if (const auto* effect = properties.Effect()) {
-    state.SetEffect(*effect);
+  const EffectPaintPropertyNodeOrAlias* effect = properties.Filter();
+  const auto* filter_clip = properties.PixelMovingFilterClipExpander();
+  if (!effect) {
+    effect = properties.Effect();
+    filter_clip = nullptr;
   }
 
-  const auto* filter_clip = properties.PixelMovingFilterClipExpander();
-  if (!filter) {
-    filter_clip = nullptr;
+  if (paint_info_.GetPaintFlags() & PaintFlag::kPrivacyPreserving) {
+    while (effect && (effect->Unalias().IsInTaintedSubtree())) {
+      filter_clip = nullptr;
+      effect = effect->Parent();
+    }
+  }
+
+  if (effect) {
+    state.SetEffect(*effect);
   }
 
   if (filter_clip) {
diff --git a/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.cc b/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.cc
index 32b9bbd..5a5fd4b4 100644
--- a/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.cc
+++ b/third_party/blink/renderer/platform/graphics/paint/effect_paint_property_node.cc
@@ -23,6 +23,9 @@
   if (!a || !b || a->output_bounds != b->output_bounds) {
     return PaintPropertyChangeType::kChangedOnlyValues;
   }
+  if (a->operations.OriginTainted() != b->operations.OriginTainted()) {
+    return PaintPropertyChangeType::kChangedOnlyValues;
+  }
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-mask-nested-filter.tentative.https.sub.html b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
new file mode 100644
index 0000000..efaf94f
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/html/canvas/element/manual/draw-element-image/privacy/svg-mask-nested-filter.tentative.https.sub.html
@@ -0,0 +1,98 @@
+<!DOCTYPE html>
+<html>
+<head>
+  <title>drawElementImage does not use cross-origin feImages on nested filtered SVG content inside 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="/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>
+      <filter id="filter-same" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <feImage href="https://{{location[host]}}/images/green-100x100.png"/>
+        <feComponentTransfer>
+          <feFuncR type="linear" slope="0" intercept="1"/>
+          <feFuncG type="linear" slope="0" intercept="1"/>
+          <feFuncB type="linear" slope="0" intercept="1"/>
+        </feComponentTransfer>
+      </filter>
+      <filter id="filter-cross" filterUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <feImage href="https://{{hosts[alt][www]}}:{{ports[https][0]}}/images/red-100x100.png"/>
+        <feComponentTransfer>
+          <feFuncR type="linear" slope="0" intercept="1"/>
+          <feFuncG type="linear" slope="0" intercept="1"/>
+          <feFuncB type="linear" slope="0" intercept="1"/>
+        </feComponentTransfer>
+      </filter>
+      <mask id="mask-same" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <g filter="url(#filter-same)">
+          <rect width="100" height="100" fill="black" filter="url(#filter-same)"/>
+        </g>
+      </mask>
+      <mask id="mask-cross" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <g filter="url(#filter-cross)">
+          <rect width="100" height="100" fill="black" filter="url(#filter-cross)"/>
+        </g>
+      </mask>
+      <mask id="mask-cross-outer" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <g filter="url(#filter-cross)">
+          <rect width="100" height="100" fill="black" filter="url(#filter-same)"/>
+        </g>
+      </mask>
+      <mask id="mask-cross-opacity" maskUnits="userSpaceOnUse" x="0" y="0" width="100" height="100">
+        <g filter="url(#filter-cross)">
+          <rect width="100" height="100" fill="black" opacity="0.5"/>
+        </g>
+      </mask>
+    </defs>
+  </svg>
+
+  <canvas id="canvas" width="100" height="400" layoutsubtree>
+    <svg id="child" width="100" height="400">
+      <svg x="0" y="0" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(20, 120, 220)" mask="url(#mask-same)"/>
+      </svg>
+      <svg x="0" y="100" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(30, 130, 230)" mask="url(#mask-cross)"/>
+      </svg>
+      <svg x="0" y="200" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(40, 140, 240)" mask="url(#mask-cross-outer)"/>
+      </svg>
+      <svg x="0" y="300" width="100" height="100">
+        <rect width="100" height="100" fill="rgb(50, 150, 250)" mask="url(#mask-cross-opacity)"/>
+      </svg>
+    </svg>
+  </canvas>
+
+  <script>
+    window.onload = () => {
+      promise_test(async function(t) {
+        await waitForCanvasPaint(canvas);
+        var ctx = canvas.getContext("2d");
+        ctx.fillStyle = "blue";
+        ctx.fillRect(0, 0, 100, 400);
+        ctx.drawElementImage(document.getElementById("child"), 0, 0);
+
+        // Fetch all pixel data once to avoid multiple slow readbacks.
+        const imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
+
+        let pixel = _getPixelFromImageData(imgData, 50, 50);
+        assert_array_equals(pixel, [20, 120, 220, 255], "Nested same origin filter images in external mask should be drawn");
+
+        pixel = _getPixelFromImageData(imgData, 50, 150);
+        assert_array_equals(pixel, [0, 0, 255, 255], "Nested cross origin filter images in external mask should not be drawn");
+
+        pixel = _getPixelFromImageData(imgData, 50, 250);
+        assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin filter image on ancestor of filtered element in external mask should not be drawn");
+
+        pixel = _getPixelFromImageData(imgData, 50, 350);
+        assert_array_equals(pixel, [0, 0, 255, 255], "Cross origin filter image on ancestor of non-filtered element with opacity in external mask should not be drawn");
+      });
+    }
+  </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.