CVE-2026-118312
Overview
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 onin2’s pixel values, a timing/observable-output side channel can leak information aboutin2. The filter-effects spec therefore requiresfeDisplacementMapto act as a pass-through whenin2(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 separateContainsBackdrop()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.)
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
- 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)
- Apply backdrop-filter
Set
backdrop-filter: url(#f)referencing an SVG filter whosefeDisplacementMaptakes the backdrop asin2(the displacement map). - Drive displacement The displacement magnitude/direction depends on the backdrop’s channel values, so rendering time / observable output varies with the secret backdrop pixels.
- Measure
Use timing (e.g.
requestAnimationFramecadence, long-task timing) to infer per-pixel backdrop values. (timing-oracle step inferred; the diff proves the unguarded data dependency, not the measurement) - Primitive Progressive recovery of cross-origin / private backdrop pixel content from the renderer — a cross-origin information disclosure.
Impact Assessment
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
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/core/paint/filter_effect_builder.cc |
modified | |
forthird_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 | |
forthird_party/blink/renderer/platform/graphics/filters/filter_effect.cc |
modified |
Files Changed
third_party/blink/renderer/core/svg/graphics/filters/svg_filter_builder.ccthird_party/blink/renderer/platform/graphics/filters/filter_effect.hthird_party/blink/renderer/platform/graphics/filters/filter_effect.ccthird_party/blink/web_tests/external/wpt/css/filter-effects/backdrop-filter-svg-displacement.htmlthird_party/blink/renderer/core/paint/filter_effect_builder.hthird_party/blink/renderer/platform/graphics/compositor_filter_operations.hthird_party/blink/renderer/platform/graphics/filters/fe_displacement_map.ccthird_party/blink/renderer/core/paint/paint_layer.ccthird_party/blink/web_tests/external/wpt/css/filter-effects/reference/backdrop-filter-svg-displacement-ref.htmlthird_party/blink/renderer/core/paint/filter_effect_builder.cc
Audit Directions
- Other data-dependent filter primitivesReview SVG filters whose output depends on input pixel values (e.g.
feDisplacementMap, lighting,feTile/feImageinteractions) for backdrop/cross-origin inputs that should force pass-through. - Flag-propagation completenessConfirm
ContainsBackdropis 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 bitsAudit everywhere
OriginTainted()is consulted to ensure the analogousContainsBackdrop()check was added where backdrop content can leak.
Patch
From 4a0ef6be08f440935e0a72fdc69806d830559491 Mon Sep 17 00:00:00 2001 From: Stephen Chenney <[email protected]> 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 <[email protected]> Commit-Queue: Stephen Chenney <[email protected]> Auto-Submit: Stephen Chenney <[email protected]> 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>
Regression Test / PoC
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>