Medium chrome Cross Origin 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactMemory corruption in Paint
DescriptionMemory corruption in Paint
ComponentPaint
Bug ClassCross Origin
Tracker540025369
Fix commit4a0ef6be08f4 (chromium/src) +107/-15
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-27

Background

`backdrop-filter`
A CSS property whose input is the rendered backdrop (whatever is painted behind the element). That backdrop can include cross-origin images, iframes, or user-sensitive form content, so its pixel contents must be treated as potentially secret.
`FEDisplacementMap`
An SVG filter primitive (feDisplacementMap) that displaces one input’s pixels using the color channels of a second input (in2). Because the displacement depends on in2’s pixel values, a timing/observable-output side channel can leak information about in2. The filter-effects spec therefore requires feDisplacementMap to act as a pass-through when in2 (the map) is origin-tainted / must not be revealed.
Origin taint vs "contains backdrop"
Blink already tracks OriginTainted() to block leaking cross-origin pixels. This change adds a separate ContainsBackdrop() bit, because simply tainting all backdrop filters would also break the legitimate HTML-in-Canvas case; the backdrop-ness must be tracked and enforced only where it creates a leak (FEDisplacementMap).
`FilterEffect` / `CompositorFilterOperations` / `FilterEffectBuilder`
The filter-effect graph nodes and builders that propagate taint/flags down the effect chain from source graphic to final effect.

Root Cause Analysis

Despite the advisory’s “Memory corruption in Paint” label, the diff contains no memory-safety sink — it is a cross-origin / backdrop information-disclosure fix enforcing a spec-mandated pass-through. Previously, a CSS backdrop-filter could reference an SVG filter containing feDisplacementMap and use the backdrop as the displacement map (in2). Since the backdrop may hold cross-origin or private content, and feDisplacementMap’s output depends on the map’s pixel values, an attacker could mount a timing attack to recover the backdrop. Blink’s existing OriginTainted tracking did not cover “this came from a backdrop”, so the displacement filter executed normally and leaked.

The patch introduces a contains_backdrop_ flag parallel to origin_tainted_ and plumbs it through the whole filter chain. FilterEffect gains ContainsBackdrop()/SetContainsBackdrop() and InputsContainBackdrop() (true if any input effect contains a backdrop); CompositorFilterOperations gains the same flag. FilterEffectBuilder::BuildFilterOperations now takes a contains_backdrop parameter and, for a backdrop filter, sets the flag on the operations and propagates it into BuildReferenceFilter (new input_contains_backdrop param), which marks the reference filter’s source graphic. svg_filter_builder.cc propagates the bit to builtin effects and to each effect whose inputs contain a backdrop. Crucially, FEDisplacementMap::MapPaintOutputColor (fe_displacement_map.cc) now returns the pass-through color when InputEffect(1)->OriginTainted() || InputEffect(1)->ContainsBackdrop() — so a backdrop map input forces the filter to pass through, exactly as the spec requires. paint_layer.cc calls BuildFilterOperations(filter_operations, /*contains_backdrop=*/true) for the backdrop-filter path. (The FilterEffect ctor was also tidied to use in-class member initializers; no behavior change.)

Key insight
feDisplacementMap was allowed to use the page’s backdrop as its displacement map; because its output depends on the map’s pixels, this leaked cross-origin/private backdrop content via a timing side channel. The fix tags backdrop-derived inputs with a ContainsBackdrop bit and forces feDisplacementMap to pass through when that bit is set.

Attack Path

  1. Compose the leak Attacker page positions sensitive cross-origin content (an image, iframe, or form) as the backdrop behind a target element. (web-reachable, standard CSS/SVG)
  2. Apply backdrop-filter Set backdrop-filter: url(#f) referencing an SVG filter whose feDisplacementMap takes the backdrop as in2 (the displacement map).
  3. Drive displacement The displacement magnitude/direction depends on the backdrop’s channel values, so rendering time / observable output varies with the secret backdrop pixels.
  4. Measure Use timing (e.g. requestAnimationFrame cadence, long-task timing) to infer per-pixel backdrop values. (timing-oracle step inferred; the diff proves the unguarded data dependency, not the measurement)
  5. Primitive Progressive recovery of cross-origin / private backdrop pixel content from the renderer — a cross-origin information disclosure.

Impact Assessment

Renderer process, no special privilege; reachable from ordinary untrusted web content via CSS backdrop-filter + SVG feDisplacementMap. The gained capability is cross-origin / sensitive information disclosure (reading backdrop content that may be cross-origin images or user form data) through a timing side channel — not memory corruption and not RCE, so the advisory’s “Memory corruption in Paint” is mislabeled relative to the diff. The leak is gated on the victim content being renderable as a backdrop and on a practical timing oracle; the companion fix CVE-2026-118338 closes the same leak on the composited-animation path.

Changed Functions

FunctionChangeNotes
if
third_party/blink/renderer/core/paint/filter_effect_builder.cc
modified
for
third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc
modified
operating_interpolation_space_
third_party/blink/renderer/platform/graphics/filters/filter_effect.cc
modified
for
third_party/blink/renderer/platform/graphics/filters/filter_effect.cc
modified

Files Changed

  • third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc
  • third_party/blink/renderer/platform/graphics/filters/filter_effect.h
  • third_party/blink/renderer/platform/graphics/filters/filter_effect.cc
  • third_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.html
  • third_party/blink/renderer/core/paint/filter_effect_builder.h
  • third_party/blink/renderer/platform/graphics/compositor_filter_operations.h
  • third_party/blink/renderer/platform/graphics/filters/fe_displacement_map.cc
  • third_party/blink/renderer/core/paint/paint_layer.cc
  • third_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.html
  • third_party/blink/renderer/core/paint/filter_effect_builder.cc

Audit Directions

  • Other data-dependent filter primitives
    Review SVG filters whose output depends on input pixel values (e.g. feDisplacementMap, lighting, feTile/feImage interactions) for backdrop/cross-origin inputs that should force pass-through.
  • Flag-propagation completeness
    Confirm ContainsBackdrop is set on every path that feeds a backdrop into a filter chain (animations handled in CVE-2026-118338; also check canvas/HTML-in-Canvas and nested reference filters).
  • Parallel taint bits
    Audit everywhere OriginTainted() is consulted to ensure the analogous ContainsBackdrop() check was added where backdrop content can leak.
From 4a0ef6be08f440935e0a72fdc69806d830559491 Mon Sep 17 00:00:00 2001
From: Stephen Chenney <schenney@chromium.org>
Date: Mon, 19 Oct 2026 14:09:47 -0700
Subject: [PATCH] Disable FEDisplacementMap acting as a backdrop-filter

The background contents filtered by a CSS backdrop filter may contain
sensitive information, including but not limited to forms containing
user data or cross-origin image content. When the background is used as
the displacement map for an SVG FEDisplacementMap filter it becomes
possible to use timing attacks to determine the background, so the spec
says the filter must operate as a pass through when the map input is
cross-origin or otherwise should not be revealed.

Unfortunately we cannot just taint all backdrop filters because doing so
disables them for HTML-in-Canvas. Instead, add tracking of "contains
backdrop" to the filter effect chain and use it in FEDisplacementMap to
enforce pass-through.

Fixed: 540025369
Change-Id: Ib2b677cbaef36965c77261a79121c73c809f6822
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8395606
Reviewed-by: Stefan Zager <szager@chromium.org>
Commit-Queue: Stephen Chenney <schenney@chromium.org>
Auto-Submit: Stephen Chenney <schenney@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1722317}
---
diff --git a/third_party/blink/renderer/core/paint/filter_effect_builder.cc b/third_party/blink/renderer/core/paint/filter_effect_builder.cc
index 2e5ae85c..63393eb 100644
--- a/third_party/blink/renderer/core/paint/filter_effect_builder.cc
+++ b/third_party/blink/renderer/core/paint/filter_effect_builder.cc
@@ -364,6 +364,9 @@
       }
       if (previous_effect->OriginTainted())
         effect->SetOriginTainted();
+      if (previous_effect->ContainsBackdrop()) {
+        effect->SetContainsBackdrop();
+      }
       previous_effect = effect;
     }
   }
@@ -371,16 +374,21 @@
 }
 
 CompositorFilterOperations FilterEffectBuilder::BuildFilterOperations(
-    const FilterOperations& operations) const {
+    const FilterOperations& operations,
+    bool contains_backdrop) const {
   InterpolationSpace current_interpolation_space = kInterpolationSpaceSRGB;
 
   CompositorFilterOperations filters;
+  if (contains_backdrop) {
+    filters.SetContainsBackdrop();
+  }
   for (FilterOperation* op : operations.Operations()) {
     switch (op->GetType()) {
       case FilterOperation::OperationType::kReference: {
         auto& reference_operation = To<ReferenceFilterOperation>(*op);
         Filter* reference_filter = BuildReferenceFilter(
-            reference_operation, nullptr, nullptr, filters.OriginTainted());
+            reference_operation, nullptr, nullptr, filters.OriginTainted(),
+            filters.ContainsBackdrop());
         if (reference_filter && reference_filter->LastEffect()) {
           // Set the interpolation space for the source of the (sub)filter to
           // match that of the previous primitive (or input).
@@ -515,7 +523,8 @@
     const ReferenceFilterOperation& reference_operation,
     FilterEffect* previous_effect,
     SVGFilterGraphNodeMap* node_map,
-    bool input_tainted) const {
+    bool input_tainted,
+    bool input_contains_backdrop) const {
   SVGResource* resource = reference_operation.Resource();
   auto* filter_element =
       DynamicTo<SVGFilterElement>(resource ? resource->Target() : nullptr);
@@ -554,6 +563,10 @@
   if (input_tainted || (previous_effect && previous_effect->OriginTainted())) {
     result->GetSourceGraphic()->SetOriginTainted();
   }
+  if (input_contains_backdrop ||
+      (previous_effect && previous_effect->ContainsBackdrop())) {
+    result->GetSourceGraphic()->SetContainsBackdrop();
+  }
 
   if (!previous_effect)
     previous_effect = result->GetSourceGraphic();
diff --git a/third_party/blink/renderer/core/paint/filter_effect_builder.h b/third_party/blink/renderer/core/paint/filter_effect_builder.h
index d09119bf..4fdc6df 100644
--- a/third_party/blink/renderer/core/paint/filter_effect_builder.h
+++ b/third_party/blink/renderer/core/paint/filter_effect_builder.h
@@ -57,12 +57,14 @@
   Filter* BuildReferenceFilter(const ReferenceFilterOperation&,
                                FilterEffect* previous_effect,
                                SVGFilterGraphNodeMap* = nullptr,
-                               bool input_tainted = false) const;
+                               bool input_tainted = false,
+                               bool input_contains_backdrop = false) const;
 
   FilterEffect* BuildFilterEffect(const FilterOperations&,
                                   bool input_tainted = false) const;
   CompositorFilterOperations BuildFilterOperations(
-      const FilterOperations&) const;
+      const FilterOperations&,
+      bool contains_backdrop = false) const;
 
   void SetShorthandScale(float shorthand_scale) {
     shorthand_scale_ = shorthand_scale;
diff --git a/third_party/blink/renderer/core/paint/paint_layer.cc b/third_party/blink/renderer/core/paint/paint_layer.cc
index 2608b8e6..2dd64a8 100644
--- a/third_party/blink/renderer/core/paint/paint_layer.cc
+++ b/third_party/blink/renderer/core/paint/paint_layer.cc
@@ -2638,7 +2638,7 @@
       FilterEffectBuilder(reference_box, FilterViewport(), zoom,
                           style.VisitedDependentColor(GetCSSPropertyColor()),
                           style.UsedColorScheme(), nullptr, nullptr)
-          .BuildFilterOperations(filter_operations);
+          .BuildFilterOperations(filter_operations, /*contains_backdrop=*/true);
   // Note that |operations| may be empty here, if the |filter_operations| list
   // contains only invalid filters (e.g. invalid reference filters). See
   // https://crbug.com/983157 for details.
diff --git a/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc b/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc
index 8f9b88b..a86204c2 100644
--- a/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc
+++ b/third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.cc
@@ -130,6 +130,11 @@
       entry.value->SetOriginTainted();
     }
   }
+  if (source_graphic->ContainsBackdrop()) {
+    for (auto& entry : builtin_effects_) {
+      entry.value->SetContainsBackdrop();
+    }
+  }
   AddBuiltinEffects();
 }
 
@@ -200,6 +205,9 @@
         ResolveInterpolationSpace(color_interpolation));
     if (effect->InputsTaintOrigin() || effect_element.TaintsOrigin())
       effect->SetOriginTainted();
+    if (effect->InputsContainBackdrop()) {
+      effect->SetContainsBackdrop();
+    }
 
     Add(AtomicString(effect_element.result()->CurrentValue()->Value()), effect);
   }
diff --git a/third_party/blink/renderer/platform/graphics/compositor_filter_operations.h b/third_party/blink/renderer/platform/graphics/compositor_filter_operations.h
index c5108991..535ed43 100644
--- a/third_party/blink/renderer/platform/graphics/compositor_filter_operations.h
+++ b/third_party/blink/renderer/platform/graphics/compositor_filter_operations.h
@@ -60,6 +60,9 @@
   void SetOriginTainted() { origin_tainted_ = true; }
   bool OriginTainted() const { return origin_tainted_; }
 
+  void SetContainsBackdrop() { contains_backdrop_ = true; }
+  bool ContainsBackdrop() const { return contains_backdrop_; }
+
   // For reference filters, this equality operator compares pointers of the
   // image_filter fields instead of their values.
   bool operator==(const CompositorFilterOperations&) const;
@@ -70,6 +73,7 @@
   cc::FilterOperations filter_operations_;
   gfx::RectF reference_box_;
   bool origin_tainted_ = false;
+  bool contains_backdrop_ = false;
 };
 
 }  // namespace blink
diff --git a/third_party/blink/renderer/platform/graphics/filters/fe_displacement_map.cc b/third_party/blink/renderer/platform/graphics/filters/fe_displacement_map.cc
index 92163f8..248b9ef 100644
--- a/third_party/blink/renderer/platform/graphics/filters/fe_displacement_map.cc
+++ b/third_party/blink/renderer/platform/graphics/filters/fe_displacement_map.cc
@@ -110,8 +110,11 @@
   // FEDisplacementMap must be a pass-through filter if
   // the origin is tainted. See:
   // https://drafts.fxtf.org/filter-effects/#fedisplacemnentmap-restrictions.
-  if (InputEffect(1)->OriginTainted())
+  // Backdrop filters also are considered origin tainting for FEDisplacementMap
+  // because the backdrop may contain private information.
+  if (InputEffect(1)->OriginTainted() || InputEffect(1)->ContainsBackdrop()) {
     return color;
+  }
 
   sk_sp<PaintFilter> displ = paint_filter_builder::Build(
       InputEffect(1), OperatingInterpolationSpace());
diff --git a/third_party/blink/renderer/platform/graphics/filters/filter_effect.cc b/third_party/blink/renderer/platform/graphics/filters/filter_effect.cc
index bcf20aa9..f0c75a9 100644
--- a/third_party/blink/renderer/platform/graphics/filters/filter_effect.cc
+++ b/third_party/blink/renderer/platform/graphics/filters/filter_effect.cc
@@ -32,11 +32,7 @@
 
 namespace blink {
 
-FilterEffect::FilterEffect(Filter* filter)
-    : filter_(filter),
-      clips_to_bounds_(true),
-      origin_tainted_(false),
-      operating_interpolation_space_(kInterpolationSpaceLinear) {
+FilterEffect::FilterEffect(Filter* filter) : filter_(filter) {
   DCHECK(filter_);
 }
 
@@ -134,6 +130,15 @@
   return false;
 }
 
+bool FilterEffect::InputsContainBackdrop() const {
+  for (const Member<FilterEffect>& effect : input_effects_) {
+    if (effect->ContainsBackdrop()) {
+      return true;
+    }
+  }
+  return false;
+}
+
 sk_sp<PaintFilter> FilterEffect::CreateTransparentBlack() const {
   sk_sp<cc::ColorFilter> color_filter =
       cc::ColorFilter::MakeBlend(SkColors::kBlack, SkBlendMode::kClear);
diff --git a/third_party/blink/renderer/platform/graphics/filters/filter_effect.h b/third_party/blink/renderer/platform/graphics/filters/filter_effect.h
index ebb1ef6..246778f 100644
--- a/third_party/blink/renderer/platform/graphics/filters/filter_effect.h
+++ b/third_party/blink/renderer/platform/graphics/filters/filter_effect.h
@@ -125,6 +125,11 @@
 
   bool InputsTaintOrigin() const;
 
+  bool ContainsBackdrop() const { return contains_backdrop_; }
+  void SetContainsBackdrop() { contains_backdrop_ = true; }
+
+  bool InputsContainBackdrop() const;
+
  protected:
   FilterEffect(Filter*);
 
@@ -160,11 +165,13 @@
 
   // Whether the effect should clip to its primitive region, or expand to use
   // the combined region of its inputs.
-  bool clips_to_bounds_;
+  bool clips_to_bounds_ = true;
 
-  bool origin_tainted_;
+  bool origin_tainted_ = false;
 
-  InterpolationSpace operating_interpolation_space_;
+  bool contains_backdrop_ = false;
+
+  InterpolationSpace operating_interpolation_space_ = kInterpolationSpaceLinear;
 
   std::array<sk_sp<PaintFilter>, 4> image_filters_;
 };
diff --git a/third_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.html b/third_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.html
new file mode 100644
index 0000000..e4f97a0
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>backdrop-filter: An SVG feDisplacement filter is pass through</title>
+<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty">
+<link rel="match"  href="reference/backdrop-filter-svg-displacement-ref.html">
+
+<div class="whitebox"></div>
+<div class="filter"></div>
+
+<svg width="0" height="0">
+  <filter id="displacement">
+    <feFlood result="flood" flood-color="green" x="50" y="50" width="100" height="100"/>
+    <feDisplacementMap in2="SourceGraphic" in="flood" xChannelSelector="G" yChannelSelector="G" scale="20"/>
+  </filter>
+</svg>
+
+<style>
+.whitebox {
+  position: absolute;
+  background: white;
+  width: 100px;
+  height: 100px;
+  top: 50px;
+  left: 50px;
+}
+.filter {
+  background-color: transparent;
+  position: absolute;
+  width: 200px;
+  height: 200px;
+  top: 0px;
+  left: 0px;
+  backdrop-filter: url(#displacement);
+}
+</style>
\ No newline at end of file
diff --git a/third_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.html b/third_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.html
new file mode 100644
index 0000000..7fe6450
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<div class="greenbox"></div>
+
+<style>
+.greenbox {
+  position: absolute;
+  background: green;
+  width: 100px;
+  height: 100px;
+  top: 50px;
+  left: 50px;
+}
+</style>
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/third_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.html b/third_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.html
new file mode 100644
index 0000000..e4f97a0
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.html
@@ -0,0 +1,35 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>backdrop-filter: An SVG feDisplacement filter is pass through</title>
+<link rel="help" href="https://drafts.fxtf.org/filter-effects-2/#BackdropFilterProperty">
+<link rel="match"  href="reference/backdrop-filter-svg-displacement-ref.html">
+
+<div class="whitebox"></div>
+<div class="filter"></div>
+
+<svg width="0" height="0">
+  <filter id="displacement">
+    <feFlood result="flood" flood-color="green" x="50" y="50" width="100" height="100"/>
+    <feDisplacementMap in2="SourceGraphic" in="flood" xChannelSelector="G" yChannelSelector="G" scale="20"/>
+  </filter>
+</svg>
+
+<style>
+.whitebox {
+  position: absolute;
+  background: white;
+  width: 100px;
+  height: 100px;
+  top: 50px;
+  left: 50px;
+}
+.filter {
+  background-color: transparent;
+  position: absolute;
+  width: 200px;
+  height: 200px;
+  top: 0px;
+  left: 0px;
+  backdrop-filter: url(#displacement);
+}
+</style>
\ No newline at end of file
diff --git a/third_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.html b/third_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.html
new file mode 100644
index 0000000..7fe6450
--- /dev/null
+++ b/third_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+
+<div class="greenbox"></div>
+
+<style>
+.greenbox {
+  position: absolute;
+  background: green;
+  width: 100px;
+  height: 100px;
+  top: 50px;
+  left: 50px;
+}
+</style>
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.