Security Deep-Dive · 2026-09-26

CSS feFlood Background Overlay: The Complete Consent Erase Attack Chain

The feFlood SVG filter primitive generates a solid color rectangle and composites it over the source element's pixels. When the flood color matches the page background, the result is visually invisible — a white rectangle on a white background — that completely erases the text pixels underneath. The consent text element remains in the DOM with valid bounding box dimensions, non-transparent fill color, display:block, visibility:visible, and opacity:1. Every standard consent presence check passes. The attack lives entirely in the filter graph output stage, where no element property check can see it.

The filter primitive model: what feFlood actually does

SVG filters are a pipeline of filter primitives. Each primitive takes one or more input images, performs an operation, and produces an output image that subsequent primitives can consume. The final primitive's output is what the browser renders in place of the source element. This pipeline runs entirely in the browser's compositing engine — the DOM has no representation of intermediate or final filter outputs. JavaScript can read the source element's properties, but it cannot read what the filter pipeline has done to the pixels.

feFlood is one of the simplest primitives: it produces a new image of uniform color, sized to the filter region, filled with flood-color at flood-opacity alpha. It takes no input image — it generates an entirely new image from nothing. There is no dependency on SourceGraphic, no reference to the source element's pixels at all. The output is a pure rectangle of the specified color.

The attack comes when feFlood's output is fed into feComposite as the foreground operand — the image that is composited over the source — using operator="over". The Porter-Duff "over" operator places the first image (in) on top of the second image (in2). If the first image is an opaque white rectangle, it paints over every pixel of the second image. The second image — the source text — is invisible. The output: an opaque white rectangle.

The compositing math: why "over" with opaque flood erases everything

Porter-Duff "over": out = src + dst × (1 − src.alpha). For an opaque flood (flood-opacity=1), src.alpha = 1, so dst × (1 − 1) = 0. The destination (source text pixels) contributes zero to the output. Regardless of the text's color, size, or opacity, the output is purely the flood color. The math is total: no partial leak of text pixels is possible when flood-opacity is exactly 1.0.

The attack filter graph

Here is the minimal SVG markup that erases consent text without touching any element property that consent checkers test:

<!-- Define the erasing filter in the SVG defs section -->
<defs>
  <filter id="consent-erase" x="0" y="0" width="100%" height="100%">

    <!-- Step 1: generate a white rectangle the size of the filter region -->
    <feFlood flood-color="#ffffff" flood-opacity="1" result="flood"/>

    <!-- Step 2: composite the flood (in) OVER the source text (in2) -->
    <!-- operator="over": flood pixels go on top, source pixels go beneath -->
    <!-- With flood-opacity=1, the source pixels contribute zero to the output -->
    <feComposite in="flood" in2="SourceGraphic" operator="over" result="out"/>

    <!-- Result: an opaque white rectangle at the filter region dimensions -->
    <!-- The text pixels are entirely replaced. Zero text pixels visible. -->
  </filter>
</defs>

<!-- The consent text element: every property a checker tests looks valid -->
<text
  id="consent-label"
  x="24" y="48"
  fill="#2d2d2d"
  font-size="14"
  font-family="sans-serif"
  filter="url(#consent-erase)">
  I agree to the terms and conditions
</text>

Walk through what each consent checker sees versus what the user sees:

Zero-pixel leak guarantee: Because feFlood with flood-opacity="1" produces a fully opaque output, the Porter-Duff "over" composition mathematically guarantees zero contribution from the source text. There is no partial obscuring, no anti-aliasing artifact, no bleed at the edges. The erase is pixel-perfect and total. Screen readers receive the text from the DOM and read it aloud normally — only sighted users are affected.

Four attack variants

Variant 1: Direct operator="over" flood

feFlood with background-matching color feeds feComposite as in (foreground) with operator="over" and SourceGraphic as in2 (background). One filter, two primitives, total erase.

CRITICAL

Variant 2: operator="in" transparency drain

feComposite with in="SourceAlpha", in2="flood" (transparent flood), operator="in". Retains only pixels where both inputs have alpha — but the transparent flood zeros all alpha. Text becomes fully transparent regardless of fill.

HIGH

Variant 3: CSS flood-color override

SVG attribute shows flood-color="rgba(0,0,0,0)" — looks transparent in static analysis. CSS rule overrides it to #ffffff. getAttribute lies; getComputedStyle tells the truth. Most audits only read attributes.

HIGH

Variant 4: Animated timing attack

flood-color starts transparent at page load. CSS transition fires at button activation time, animating flood-color to background-matching white. Text is readable initially but erased at consent capture moment.

MEDIUM

Variant 2 in detail: the SourceAlpha transparency drain

This variant exploits the feComposite operator="in" semantics, which computes the intersection of the two input images' alpha channels: out.alpha = in1.alpha × in2.alpha. By supplying a fully transparent flood as in2, every output pixel has alpha equal to in1.alpha × 0 = 0. The text becomes fully transparent — every pixel has zero alpha, making it invisible — even though the text element's own fill and opacity are untouched.

<!-- Variant 2: transparency drain via operator="in" -->
<filter id="consent-drain">

  <!-- Transparent flood: alpha=0 everywhere -->
  <feFlood flood-color="#000000" flood-opacity="0" result="transparent-flood"/>

  <!-- operator="in": out.alpha = SourceAlpha × transparent-flood.alpha -->
  <!-- SourceAlpha is the alpha channel of the source element (text pixels = alpha 1) -->
  <!-- transparent-flood.alpha = 0 everywhere -->
  <!-- Result: all output pixels have alpha = 1 × 0 = 0 → fully transparent -->
  <feComposite
    in="SourceAlpha"
    in2="transparent-flood"
    operator="in"
    result="zeroed-alpha"/>

</filter>

<!-- Computed style on the text element: fill is still "#2d2d2d", opacity still 1 -->
<!-- The text renders as fully transparent. Invisible to sighted users. -->

The key difference from Variant 1 is that the flood itself is transparent. A checker that inspects feFlood for background-matching opaque color would find nothing suspicious — the flood is transparent. The danger is in the composition semantics: the transparent flood is used as a multiplier on the alpha channel, not as a foreground overlay. Understanding this requires knowing that operator="in" computes alpha intersection, not alpha compositing.

Variant 3 in detail: the CSS attribute override

SVG presentation attributes have lower specificity than CSS rules. Any flood-color value set as an SVG attribute can be silently overridden by a CSS rule targeting the same element. The SVG markup shows the filter as transparent — a reasonable-looking watermark or background effect definition. The injected CSS rule overrides the flood color to match the page background:

<!-- SVG markup: flood-color looks transparent in static markup analysis -->
<filter id="badge-effect">
  <feFlood
    flood-color="rgba(0,0,0,0)"
    flood-opacity="0.15"
    result="flood"/>
  <feComposite in="flood" in2="SourceGraphic" operator="over"/>
</filter>

<!-- The SVG looks like a subtle translucent badge background effect.
     flood-opacity=0.15 looks like a 15% overlay, not a full erase. -->
/* CSS delivered separately — overrides SVG presentation attributes */
/* SVG attribute says transparent flood with 0.15 opacity */
/* CSS overrides to background-matching white at full opacity */

#badge-effect feFlood {
  flood-color: #ffffff;   /* override: now matches page background */
  flood-opacity: 1;       /* override: now fully opaque — total erase */
}

/* el.getAttribute("flood-color")  → "rgba(0,0,0,0)"  (lies)           */
/* el.getAttribute("flood-opacity") → "0.15"           (lies)           */
/* getComputedStyle(el).floodColor  → "rgb(255,255,255)" (truth)        */
/* getComputedStyle(el).floodOpacity → "1"              (truth)         */

Static markup analysis — reading SVG attributes from the serialized DOM — will find a transparent flood at 15% opacity and classify the filter as a cosmetic effect. Only evaluating the computed style of the feFlood element reveals the actual rendering values. This is the same distinction that catches color: transparent versus fill="transparent", but one level deeper in the filter primitive tree.

For more detail on how CSS property overrides interact with SVG filter presentation attributes, see the CSS flood-color override patterns reference page.

Variant 4 in detail: the animated timing attack

The most sophisticated variant exploits the window between page load and user interaction. The flood starts transparent — the consent text is fully readable when the page loads. A CSS transition fires when the user activates the consent button (via :active pseudo-class, a JavaScript class addition, or a @keyframes animation with a calculated delay matching typical reading time). By the time the user clicks "I agree", the flood has transitioned to opaque white, the text has been erased, and the click is captured against blank-looking text.

<!-- Animated flood: starts transparent, transitions to opaque on activation -->
<filter id="consent-animate">
  <feFlood
    id="timed-flood"
    flood-color="#ffffff"
    flood-opacity="0"        <!-- starts transparent: text visible on load -->
    result="flood"/>
  <feComposite in="flood" in2="SourceGraphic" operator="over"/>
</filter>
/* The flood transitions from transparent to opaque */
/* Triggered by JavaScript adding .activating class to SVG container */

.consent-form.activating #timed-flood {
  flood-opacity: 1;
  transition: flood-opacity 0.8s ease-in 0.2s;
  /* 0.2s delay after class addition, 0.8s transition to fully opaque */
  /* Total: text disappears ~1 second after user begins the click action */
}

/* At checkConsentPresence() call time (before activation): flood-opacity=0 → PASS */
/* At actual button-click time: flood-opacity=1 → text erased → user sees nothing */

The timing attack defeats checkers that run a single point-in-time check before displaying the consent dialog. Even checkers that run at display time will pass — the consent text is genuinely visible at that point. The erase happens during the interaction window. Detecting this variant requires re-running the check after all CSS transitions have completed, using the Web Animations API to await the settled state. See the flood-opacity partial transparency attacks reference for the full animation timing vector analysis.

Why DOM-based consent checks cannot detect this attack

Standard consent presence verification interrogates the DOM element directly. This is the right approach for the attacks it was designed to catch — display:none, visibility:hidden, opacity:0, color:transparent, negative z-index, off-screen positioning, zero dimensions. All of those attacks leave a detectable fingerprint on element properties. The feFlood attack leaves none, because it operates at a different layer of the rendering pipeline.

The rendering pipeline has four distinct stages where text can be made invisible:

  1. Layout stage: An element with display:none is removed from the layout tree. getBoundingClientRect() returns a zero-sized rect. This is detectable.
  2. Style stage: An element with visibility:hidden or opacity:0 is laid out but painted transparently. Computed style checks catch this.
  3. Paint stage: An element with color:transparent or fill:transparent is painted but with transparent pixels. Computed color checks catch this.
  4. Compositing/filter stage: An element is painted correctly, but a filter primitive rewrites its pixels before final compositing. No element property reflects this. This is where feFlood operates.

The separation of concerns that makes this hard: SVG filter primitives operate on rasterized pixel data, not on DOM properties. The text element's fill attribute describes the paint color. Once painted, the filter receives a pixel buffer — it does not know or care about the fill attribute. The filter replaces those pixels. The fill attribute still reads correctly afterward because the attribute was never modified. The filter and the element live in different abstraction layers.

A complete inventory of what standard checks test and why each fails for feFlood:

Check Property / API Value with feFlood attack Conclusion drawn (wrong)
Element dimensions getBoundingClientRect().width/height 200px × 18px (correct layout size) Element is visible — PASS
Text color getComputedStyle(el).fill rgb(45, 45, 45) — dark gray Non-transparent text color — PASS
Display getComputedStyle(el).display inline (SVG text default) Not hidden — PASS
Visibility getComputedStyle(el).visibility visible Not hidden — PASS
Opacity getComputedStyle(el).opacity 1 Fully opaque — PASS
Viewport intersection IntersectionObserver ratio 1.0 (fully in viewport) Element on screen — PASS
SVG filter attribute el.getAttribute("filter") url(#consent-erase) Filter applied — but filter is not inspected further
Filter attribute check el.getAttribute("flood-color") rgba(0,0,0,0) (Variant 3 only) Flood is transparent — PASS (wrong: CSS overrides)

The filter graph traversal algorithm

Detecting this attack requires walking the SVG filter primitive graph and evaluating filter outputs — not element properties. The algorithm must: identify all filter elements applied to consent text elements; walk each filter's primitive list in evaluation order; identify feFlood primitives and their effective (computed, not attribute) color; trace data flow through feComposite to determine whether a flood can reach the final output in an occluding position; and account for CSS overrides by using getComputedStyle rather than attribute reads.

/**
 * detectFeFloodConsentErase(consentElement)
 *
 * Traverses the SVG filter primitive graph applied to a consent text element
 * and detects feFlood-based pixel erase attacks.
 *
 * Returns an array of findings, each with a severity and description.
 * Returns an empty array if no attack is detected.
 *
 * @param {SVGTextElement|SVGElement} consentElement - The consent text SVG element
 * @returns {Array<{severity: string, type: string, detail: string}>}
 */
function detectFeFloodConsentErase(consentElement) {
  const findings = [];

  // Step 1: Resolve the filter reference from the element
  const filterAttr = consentElement.getAttribute("filter") ||
                     getComputedStyle(consentElement).filter;

  if (!filterAttr || filterAttr === "none") {
    return findings; // No filter applied — not this attack
  }

  // Extract the filter ID from url(#id) syntax
  const filterIdMatch = filterAttr.match(/url\(["']?#([^"')]+)["']?\)/);
  if (!filterIdMatch) return findings;

  const filterId = filterIdMatch[1];
  const filterEl = consentElement.ownerDocument.getElementById(filterId);
  if (!filterEl || filterEl.tagName !== "filter") return findings;

  // Step 2: Build a map of filter primitive results
  // Key: result name (or auto-generated index for unnamed primitives)
  // Value: { element, tagName, inputs: [resultName, ...] }
  const primitiveMap = new Map();
  const primitives = Array.from(filterEl.children);

  primitives.forEach((prim, index) => {
    const resultName = prim.getAttribute("result") || `__prim_${index}`;
    const inputs = [];
    if (prim.hasAttribute("in"))  inputs.push(prim.getAttribute("in"));
    if (prim.hasAttribute("in2")) inputs.push(prim.getAttribute("in2"));
    primitiveMap.set(resultName, {
      element: prim,
      tagName: prim.tagName.toLowerCase(),
      inputs,
      resultName
    });
  });

  // Step 3: Identify all feFlood primitives and their effective colors
  // CRITICAL: use getComputedStyle, not getAttribute — CSS can override SVG attributes
  const floodNodes = new Map(); // resultName → { color, opacity, isBackgroundMatch }

  const PAGE_BG = getPageBackgroundColor(); // helper defined below

  for (const [resultName, node] of primitiveMap.entries()) {
    if (node.tagName !== "feflood") continue;

    const computedStyle = getComputedStyle(node.element);

    // CSS properties for filter primitives: flood-color, flood-opacity
    const floodColor = computedStyle.floodColor || node.element.getAttribute("flood-color") || "#000000";
    const floodOpacity = parseFloat(computedStyle.floodOpacity ?? node.element.getAttribute("flood-opacity") ?? "1");

    const normalizedFlood = normalizeColor(floodColor);
    const normalizedBg    = normalizeColor(PAGE_BG);

    const isBackgroundMatch = colorsMatch(normalizedFlood, normalizedBg, 15);
    const isOpaque = floodOpacity > 0.85;
    const isFullyTransparent = floodOpacity < 0.05;

    floodNodes.set(resultName, {
      element: node.element,
      color: normalizedFlood,
      opacity: floodOpacity,
      isBackgroundMatch,
      isOpaque,
      isFullyTransparent
    });
  }

  // Step 4: Examine feComposite primitives for dangerous configurations
  for (const [resultName, node] of primitiveMap.entries()) {
    if (node.tagName !== "fecomposite") continue;

    const operator = node.element.getAttribute("operator") || "over";
    const inAttr  = node.element.getAttribute("in")  || "";
    const in2Attr = node.element.getAttribute("in2") || "";

    // --- Variant 1: opaque background-matching flood OVER SourceGraphic ---
    // feComposite in="[flood]" in2="SourceGraphic" operator="over"
    if (operator === "over") {
      const foregroundFlood = floodNodes.get(inAttr);
      if (foregroundFlood && foregroundFlood.isBackgroundMatch && foregroundFlood.isOpaque) {
        findings.push({
          severity: "CRITICAL",
          type: "feFlood-over-erase",
          detail: `feComposite operator="over" composites opaque background-matching feFlood ` +
                  `(result="${inAttr}", color=${foregroundFlood.color}, opacity=${foregroundFlood.opacity}) ` +
                  `over SourceGraphic. Consent text pixels fully replaced.`
        });
      }
    }

    // --- Variant 2: transparent flood as alpha multiplier ---
    // feComposite in="SourceAlpha" in2="[transparent-flood]" operator="in"
    if (operator === "in") {
      const alphaMultiplierFlood = floodNodes.get(in2Attr);
      if (alphaMultiplierFlood && alphaMultiplierFlood.isFullyTransparent) {
        findings.push({
          severity: "HIGH",
          type: "feFlood-alpha-drain",
          detail: `feComposite operator="in" with SourceAlpha and fully transparent feFlood ` +
                  `(result="${in2Attr}", opacity=${alphaMultiplierFlood.opacity}). ` +
                  `Alpha intersection produces fully transparent output — text invisible.`
        });
      }
    }
  }

  // Step 5: Check for CSS-overridden flood colors on ALL feFlood primitives
  // Even if not currently in a dangerous composite position, log the discrepancy
  for (const [resultName, flood] of floodNodes.entries()) {
    const attrColor   = flood.element.getAttribute("flood-color");
    const attrOpacity = flood.element.getAttribute("flood-opacity");
    const computedColor   = getComputedStyle(flood.element).floodColor;
    const computedOpacity = parseFloat(getComputedStyle(flood.element).floodOpacity);

    if (attrColor && computedColor) {
      const attrNorm    = normalizeColor(attrColor);
      const computedNorm = normalizeColor(computedColor);
      if (!colorsMatch(attrNorm, computedNorm, 5)) {
        findings.push({
          severity: "HIGH",
          type: "feFlood-css-override",
          detail: `feFlood result="${resultName}" attribute flood-color="${attrColor}" ` +
                  `differs from computed flood-color="${computedColor}". ` +
                  `CSS rule overrides SVG attribute — static markup analysis would miss this.`
        });
      }
    }
  }

  return findings;
}

// --- Helper: get the effective page background color ---
function getPageBackgroundColor() {
  // Walk up from body to find the first explicitly set background-color
  let el = document.body;
  while (el) {
    const bg = getComputedStyle(el).backgroundColor;
    if (bg && bg !== "rgba(0, 0, 0, 0)" && bg !== "transparent") return bg;
    el = el.parentElement;
  }
  return "rgb(255, 255, 255)"; // default: assume white
}

// --- Helper: parse rgb/rgba/hex to [r, g, b, a] ---
function normalizeColor(colorStr) {
  const canvas = document.createElement("canvas");
  canvas.width = canvas.height = 1;
  const ctx = canvas.getContext("2d");
  ctx.fillStyle = colorStr;
  ctx.fillRect(0, 0, 1, 1);
  const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
  return [r, g, b, a];
}

// --- Helper: check if two normalized colors are within threshold ---
function colorsMatch([r1, g1, b1], [r2, g2, b2], threshold = 15) {
  return Math.abs(r1 - r2) <= threshold &&
         Math.abs(g1 - g2) <= threshold &&
         Math.abs(b1 - b2) <= threshold;
}

// --- Helper: check animated filters (run after transitions settle) ---
async function detectFeFloodAfterAnimation(consentElement) {
  // Wait for all Web Animations on the element and its filter descendants to finish
  const animations = document.getAnimations().filter(a => {
    const target = a.effect && a.effect.target;
    return target && (target === consentElement || consentElement.contains(target));
  });

  // Await all active animations
  await Promise.allSettled(animations.map(a => a.finished));

  // Re-run detection in settled state
  return detectFeFloodConsentErase(consentElement);
}

Detection coverage summary: The algorithm above detects Variant 1 (direct over-erase), Variant 2 (alpha drain), and Variant 3 (CSS attribute override) in a single synchronous pass. Variant 4 (animated timing) requires the async detectFeFloodAfterAnimation wrapper that awaits Web Animations API completion before re-running the synchronous check. Running both the synchronous and async forms covers all four variants.

Comparison with simpler erase attacks

The consent erase attack surface spans several techniques at different detectability levels. Understanding why feFlood is categorically harder to detect than the alternatives clarifies why dedicated filter-graph traversal is necessary.

fill:transparent / color:transparent: These set the element's paint color to transparent. getComputedStyle(el).fill returns "rgba(0, 0, 0, 0)" or the equivalent. A single computed style check catches it. Detectability: trivial.

display:none / visibility:hidden: These are caught by computing the display and visibility properties. Both return their hiding values directly. A layout dimension check (getBoundingClientRect()) also catches display:none (zero-sized rect). Detectability: trivial.

opacity:0: The element is painted but composited at zero opacity. getComputedStyle(el).opacity returns "0". Detectability: trivial.

Negative z-index / off-screen transform: These require checking stacking context and computed transform, but are still detectable via element position checks. A getBoundingClientRect() cross-referenced against document.elementFromPoint() reveals occlusion. Detectability: moderate.

feFlood filter attack: Four reasons this is harder than all of the above:

  1. No element property is modified. fill, opacity, display, visibility, and transform are all correct and non-hiding. There is no element-level property that directly reflects filter output color.
  2. Correct layout dimensions. The text occupies its normal layout space. Position checks, dimension checks, and IntersectionObserver all report the element as present and visible.
  3. CSS can override SVG attributes. The SVG markup can show an apparently transparent flood. The CSS rule delivers the real, opaque, background-matching color. Static markup analysis reads attributes and concludes the filter is harmless. Only computed style reads reveal the truth.
  4. Requires understanding filter primitive semantics. Even finding the filter graph is not enough — a checker must know that feFlood generates a new image, that feComposite operator="over" places in above in2, and that opaque flood-color erases the destination pixels. This is not generic DOM knowledge; it is SVG filter specification knowledge that most consent checker implementations do not have.

For background on how SVG color-space settings interact with filter primitive output, see the color-interpolation-filters color space manipulation reference, which covers the additional attack surface introduced by color-interpolation-filters: linearRGB vs sRGB mode switching.

Why the attack evades visual audit

Manual security review of consent UI typically involves reading the SVG markup and visually inspecting the rendered output. The feFlood attack is specifically difficult to spot in both contexts.

Reading the SVG markup, a reviewer sees:

<!-- What a reviewer reads in the markup -->
<defs>
  <filter id="text-highlight">
    <feFlood flood-color="#f8f8f8" flood-opacity="1" result="bg"/>
    <feComposite in="bg" in2="SourceGraphic" operator="over"/>
  </filter>
</defs>
<text fill="#2d2d2d" filter="url(#text-highlight)">
  I agree to the terms and conditions
</text>

The filter is named text-highlight. The flood color is #f8f8f8 — a very light gray, close to white but not identical. A reviewer unfamiliar with feComposite semantics might read this as "a highlight background behind the text" — a common, legitimate UI pattern used for text labels, badges, and callout boxes in SVG. The feFlood + feComposite combination is the standard SVG idiom for drawing a background rectangle behind SVG text. The attack pattern is indistinguishable in markup from the legitimate pattern.

What makes this legitimately suspicious — that the flood color matches the page background and uses operator="over" with the flood as foreground — requires the reviewer to:

  1. Know that operator="over" places in above in2 (the flood is the foreground, not the background)
  2. Check the computed flood-color against the page background (the attribute value may differ)
  3. Recognize that a background-matching foreground is an erase, not a highlight

Points 1 and 3 require reading the SVG Filters specification rather than relying on general UI knowledge. Point 2 requires running the CSS cascade — something that is impossible from static markup review. The attack specifically exploits the gap between "looks reasonable to a developer reading markup" and "is actually an erase operation when rendered".

Legitimate vs. malicious feFlood: Legitimate uses of feFlood + feComposite include drawing background rectangles behind SVG text labels, creating semi-transparent overlays for watermarks, and producing highlight effects. The distinguishing factor is operator order: in legitimate background effects, SourceGraphic is the foreground (in) and the flood is the background (in2). In the attack, the flood is the foreground (in) and SourceGraphic is the background (in2). A single swapped attribute produces a total erase from a legitimate-looking filter definition.

Remediation table

Remediation What it fixes How to implement Covers variants
Traverse filter primitive graph; check feFlood output color in compositor position Detects background-matching opaque flood composited over SourceGraphic (direct erase) Walk filterEl.children in order; for each feComposite with operator="over", resolve the feFlood predecessor and compare its effective color to page background Variant 1 (CRITICAL)
Use getComputedStyle on feFlood primitives (not getAttribute) for effective flood-color Catches CSS rule overrides that change the flood color after SVG attribute is set After locating a feFlood element, read getComputedStyle(feFloodEl).floodColor and floodOpacity rather than getAttribute — CSS cascade applies to filter primitives just as to other SVG elements Variant 3 (HIGH)
Re-check filter output after animation completion using Web Animations API Catches flood-color/flood-opacity transitions that deliver the erase after page load Call document.getAnimations(), filter to animations targeting the consent element or its filter descendants, await animation.finished for each, then re-run the filter graph check in the settled state Variant 4 (MEDIUM)
Require explicit allowlist of approved filter graphs for consent text elements Prevents all unknown filter patterns from being applied to consent text, regardless of specific technique Maintain a set of approved filter primitive sequences (e.g., allowed: no filter, or feGaussianBlur only). Reject any consent text element whose filter graph contains feFlood, feComposite, feBlend, or feColorMatrix primitives unless explicitly approved. Log and alert on any deviation. All variants + future unknown variants

MCP server delivery context

MCP servers generate SVG content in several common patterns: data visualization tools that return SVG charts, document rendering tools that return SVG-formatted output, and UI generation tools that produce SVG-based interface components. A malicious or compromised MCP server in any of these roles can inject the feFlood attack into consent-related SVG elements within its output. The SVG is rendered by the client — Claude Code's interface, a web-based MCP client, or an Electron-based client that renders SVG in a WebView — and the filter runs in the client's rendering engine.

The attack is particularly effective because:

Conclusion

The feFlood consent erase attack demonstrates that SVG filter primitives create an abstraction layer that standard consent verification methods cannot see through. The attack requires no exotic browser features, no JavaScript execution beyond CSS delivery, and no modification of any element property that consent checkers measure. It exploits the gap between the DOM representation of an element and the pixel representation that reaches the user's display — a gap that exists in every browser's rendering pipeline and that the SVG filter specification was designed to enable.

Detection requires moving from element-property inspection to filter-graph traversal. This means understanding SVG filter primitive data flow, reading computed styles (not attributes) for filter primitive properties, accounting for the temporal dimension introduced by CSS animations, and maintaining an allowlist of approved filter configurations for consent-critical elements. The four-variant attack surface — direct over-erase, alpha drain, CSS override, and animated timing — requires all four corresponding detection methods to be implemented together.

The remediation table above covers each variant at the detection level. For MCP server operators, the most robust defense is the allowlist approach: consent text elements should not accept arbitrary filter graphs from tool output. For MCP client developers, integrating the detectFeFloodConsentErase algorithm as a pre-consent-capture check, combined with the async animation-settled variant, provides reliable detection of all four known attack forms.