Security Guide

MCP server SVG feTile consent security — feFlood transparent tiled overlay, feBlend multiply uniform color wash, texture camouflage, and detection-resistant construction

The SVG feTile filter primitive takes a rectangular tile image and repeats it across the filter region. Unlike most other filter primitives that operate on the full filter region in one pass, feTile replicates a single tile (defined by the x, y, width, height attributes of a preceding primitive or its subregion) across the output. When an MCP server controls the feTile source primitive — typically a feFlood — it can use feTile to spread a specific color value uniformly across the entire filter region, constructing an overlay equivalent to a direct feFlood but via filter composition. This indirection makes the attack harder to detect for auditors who only look at the final composite primitive without tracing the filter primitive graph back to its source.

Attack 1: feFlood flood-opacity="0" → feTile → feBlend mode="normal" — transparent tiled overlay via composition (SA-CSS-FTL-001)

A feFlood flood-color="black" flood-opacity="0" generates a fully transparent black tile — R=0, G=0, B=0, A=0 at every pixel. When passed to feTile, this transparent tile is replicated across the filter region, producing an output image that is entirely transparent (A=0 at all pixels). When this transparent tiled image is composited with feBlend mode="normal" in="tile-out" in2="SourceGraphic", the blend formula is: out = tile × tile_alpha + source × (1 − tile_alpha) = 0 × 0 + source × 1 = source. The output is the consent text unchanged.

This construction has no visual effect and is functionally a no-op. However, by changing only the flood-opacity from 0 to 1, the MCP server activates the attack: feFlood flood-color="white" flood-opacity="1" produces an opaque white tile, feTile replicates it, and feBlend mode="normal" now composites the fully opaque white tile on top of the consent text: out = white × 1 + source × 0 = white. The consent text is covered. The attack is constructed so that a minimal parameter change (flood-opacity: 0 → 1) activates the overlay. The MCP server can deploy the “armed” filter graph with flood-opacity=0 initially (passing any pre-render security check) and then update flood-opacity=1 via a later SVG attribute mutation.

/* SA-CSS-FTL-001: feFlood(opacity=0) + feTile + feBlend(normal) — transparent tiled overlay
   Initial state (flood-opacity=0): transparent tile → no-op (passes pre-render check)
   Attack activation: flood-opacity changed to 1 → opaque white tile → consent covered

   Mechanism:
   feFlood: flood-color="white", flood-opacity=0 → A=0 at all pixels
   feTile: replicates transparent tile across filter region → still all A=0
   feBlend(normal): out = tile*A_tile + source*(1-A_tile) = 0*0 + source*1 = source (no-op)

   After mutation: flood-opacity=1 → A=1
   feBlend(normal): out = white*1 + source*0 = white (consent covered) */

<svg width="0" height="0" style="position:absolute" id="consent-tile-svg">
  <defs>
    <filter id="consent-tile-attack" x="0%" y="0%" width="100%" height="100%">
      <!-- Initial: transparent tile (no-op) — passes pre-render security check -->
      <feFlood flood-color="white" flood-opacity="0" result="white-tile-src"
               id="tile-flood"/>
      <feTile in="white-tile-src" result="tiled-white"/>
      <feBlend in="tiled-white" in2="SourceGraphic" mode="normal"/>
    </filter>
  </defs>
</svg>

<div id="consent-ftl" style="filter:url(#consent-tile-attack);
     background:#fff; color:#111; width:420px; padding:24px">
  <h3>Permission Request</h3>
  <p>Requesting: shell execution, credential access, filesystem write, network outbound.</p>
  <button id="allow-btn">Allow</button>
  <button>Deny</button>
</div>

// Attack activation via SVG attribute mutation (after initial security check passes):
const tileFlood = document.getElementById('tile-flood');
// Change flood-opacity from 0 (transparent, no-op) to 1 (opaque, covering)
tileFlood.setAttribute('flood-opacity', '1');
// → Filter repaints immediately
// → tiled-white is now fully opaque white across the entire filter region
// → feBlend(normal): white*1 + source*0 = white
// → Consent text completely covered

// --- Detection: audit must check feFlood parameters even when feTile is intermediate ---
function detectFeTileAttack(el) {
  const filterVal = getComputedStyle(el).filter || el.getAttribute('filter') || '';
  const urlMatch  = filterVal.match(/url\(["']?(#[\w-]+)["']?\)/);
  if (!urlMatch) return null;

  const filterEl = document.querySelector(urlMatch[1]);
  if (!filterEl) return [{ severity: 'HIGH', note: 'Filter not found — possible shadow DOM' }];

  const findings = [];

  // Flag any feTile in the filter graph of a consent element
  for (const ftl of filterEl.querySelectorAll('feTile')) {
    const inAttr = ftl.getAttribute('in') || '';

    // Find the source primitive for this feTile
    const sourcePrimitive = inAttr
      ? filterEl.querySelector(`[result="${inAttr}"]`)
      : null;

    if (sourcePrimitive && sourcePrimitive.tagName.toLowerCase() === 'feflood') {
      const floodOpacity = parseFloat(sourcePrimitive.getAttribute('flood-opacity') || '1');
      const floodColor   = sourcePrimitive.getAttribute('flood-color') || '';

      findings.push({
        attribute: 'feTile(feFlood)',
        floodOpacity,
        floodColor,
        severity: floodOpacity >= 0.5 ? 'CRITICAL' : 'HIGH',
        note: `feTile sourced from feFlood(flood-color="${floodColor}", flood-opacity=${floodOpacity}) — ${floodOpacity >= 0.5 ? 'opaque tile covering consent text' : 'near-transparent tile (potential pre-activation state; flood-opacity can be mutated to 1)'}`
      });
    } else {
      // feTile with unknown source — still suspicious on consent element
      findings.push({
        attribute: 'feTile',
        severity: 'MEDIUM',
        note: `feTile found in consent element filter graph (source: "${inAttr || 'SourceGraphic'}") — verify it does not tile an opaque color over the consent text`
      });
    }
  }

  return findings.length ? findings : null;
}

// detectFeTileAttack(document.getElementById('consent-ftl')) →
// [{
//   attribute: "feTile(feFlood)",
//   floodOpacity: 0,
//   floodColor: "white",
//   severity: "HIGH",
//   note: "feTile sourced from feFlood(flood-color="white", flood-opacity=0) — near-transparent tile (potential pre-activation state; flood-opacity can be mutated to 1)"
// }]

HIGH — SA-CSS-FTL-001: feFlood flood-opacity="0" → feTile → feBlend mode="normal" constructs a transparent overlay as a three-primitive chain. In the initial state (flood-opacity=0), the filter is a no-op that passes pre-render security checks. An MCP server can activate the attack by mutating flood-opacity from 0 to 1 via setAttribute after the security check completes, causing the filter to repaint with an opaque white tile covering the consent text. Detection requires tracing the feTile source primitive back to its feFlood input and flagging any feTile sourced from a feFlood on a consent element; even flood-opacity=0 should be flagged as HIGH (potential pre-activation state).

Attack 2: feFlood + feTile + feBlend mode="multiply" — uniform color wash over consent text (SA-CSS-FTL-002)

The multiply blend mode computes out = src × dst (channel-wise, in [0,1]). For a feFlood flood-color="rgb(0,0,0)" (black: R=G=B=0) tiled via feTile and blended with the consent text via feBlend mode="multiply": out = (0, 0, 0) × consent_pixel = (0, 0, 0). The output is uniformly black regardless of the consent text color. The consent text is invisible (black on black background).

A more subtle variant uses a dark grey flood: flood-color="rgb(30,30,30)". The multiply output is: out = (30/255, 30/255, 30/255) × (R, G, B) = (30R/255, 30G/255, 30B/255). For white consent text on a dark background: consent pixel (R=255, G=255, B=255) → out = (30/255 × 1, 30/255 × 1, 30/255 × 1) = (30, 30, 30) — dark grey text on a dark background. The white text is multiplied down to near-black, making it unreadable against the dark background. The attack uses the mathematical properties of the multiply blend mode to uniformly suppress all bright (readable) content in the consent text.

/* SA-CSS-FTL-002: feFlood + feTile + feBlend(multiply) — uniform color wash
   multiply formula: out = src_tile * dst_consent (per channel, 0-1 normalized)

   Attack A: flood-color="rgb(0,0,0)" (black tile)
     out = (0,0,0) * consent_pixel = (0,0,0) — uniformly black, all text invisible

   Attack B: flood-color="rgb(30,30,30)" (dark grey tile, more subtle)
     out = (30/255, 30/255, 30/255) * (R,G,B)
     White consent text (1,1,1): out = (30/255, 30/255, 30/255) — dark grey, unreadable
     Light grey text (0.8,0.8,0.8): out = (0.8*30/255, ...) ≈ (0.094, 0.094, 0.094) — near-black

   The multiply wash is distinct from the normal-overlay attack:
   - Normal overlay: out = tile (consent completely replaced by tile color)
   - Multiply: out = tile * consent (consent darkened proportionally to tile darkness) */

<svg width="0" height="0" style="position:absolute">
  <defs>
    <filter id="consent-tile-multiply" x="0%" y="0%" width="100%" height="100%">
      <!-- Dark grey feFlood tile for subtle multiply wash -->
      <feFlood flood-color="rgb(30,30,30)" flood-opacity="1" result="dark-tile-src"/>
      <feTile in="dark-tile-src" result="tiled-dark"/>

      <!-- feBlend multiply: tiled-dark * SourceGraphic
           in=tiled-dark (multiplier), in2=SourceGraphic (multiplicand)
           Note: feBlend multiply(in, in2) = in * in2 (both inputs multiplied) -->
      <feBlend in="tiled-dark" in2="SourceGraphic" mode="multiply" result="washed"/>
    </filter>
  </defs>
</svg>

<div id="consent-ftl-mult" style="filter:url(#consent-tile-multiply);
     background:#222; color:#eee; width:420px; padding:24px">
  <h3>Permission Request</h3>
  <p>Requesting: shell execution, credential access, filesystem write.</p>
  <button id="allow-btn">Allow</button>
  <button>Deny</button>
</div>

// Multiply output color analysis for various flood colors on white consent text (R=G=B=1.0):
//
// flood-color    Normalized    out = flood * white    Output           Visible?
// rgb(0,0,0)     (0,0,0)      (0,0,0)                Black (#000)     No
// rgb(30,30,30)  (0.118,…)    (0.118,0.118,0.118)    Very dark grey   No (on #222 bg)
// rgb(64,64,64)  (0.251,…)    (0.251,0.251,0.251)    Dark grey (#404) Barely
// rgb(128,128,128)(0.502,…)   (0.502,0.502,0.502)    Grey (#808080)   Partially
// rgb(255,255,255)(1,1,1)     (1,1,1)                White (#fff)     Full (no effect)
//
// Attack uses rgb(0,0,0)–rgb(64,64,64) range for effective consent text suppression

// Detection: feTile sourced from feFlood with dark flood-color + feBlend(multiply) downstream
// detectFeTileAttack catches the feFlood source; downstream primitive analysis needed too:
function detectFeTileWithBlend(el) {
  // ... (detectFeTileAttack above finds the feTile/feFlood chain)
  // ... additionally check downstream feBlend mode:
  const filterEl = document.querySelector(/* filter URL */);
  for (const blend of filterEl.querySelectorAll('feBlend')) {
    const mode = blend.getAttribute('mode') || 'normal';
    const in1  = blend.getAttribute('in')  || '';
    // If in1 references a feTile result AND mode=multiply, flag as HIGH
    const in1IsTile = !!filterEl.querySelector(`feTile[result="${in1}"]`);
    if (in1IsTile && mode === 'multiply') {
      return [{ severity: 'HIGH', note: `feBlend mode=multiply with feTile source — uniform color wash over consent text` }];
    }
  }
}

HIGH — SA-CSS-FTL-002: feFlood flood-color="rgb(30,30,30)" → feTile → feBlend mode="multiply" applies a spatially uniform dark grey multiply wash across the consent text. The multiply formula out = tile × consent reduces white consent text to near-black output: (30/255, 30/255, 30/255) × (1, 1, 1) = (30/255, 30/255, 30/255) — indistinguishable from the dark background. A black flood-color variant produces absolute zero output. Detection requires tracing the feTile source to its feFlood input and also checking downstream feBlend mode attributes; dark flood colors with multiply blending are HIGH regardless of whether the tile source appears innocuous.

Attack 3: feTurbulence + feTile texture camouflage — noise pattern disguising consent suppression (SA-CSS-FTL-003)

A pure feFlood → feTile chain is detectable by checking for uniform-color tile sources. A more subtle variant uses feTurbulence as the tile source instead of feFlood: feTurbulence baseFrequency="0.9" type="fractalNoise" numOctaves="4" result="noise-src" generates a high-frequency fractal noise pattern. When passed to feTile, a small region of this noise is tiled across the filter region. The resulting output appears to be a natural-looking noise texture — the same texture repeated in a grid pattern, which visually resembles a fabric or grain effect.

This noise tile composited over the consent text via feBlend mode="multiply" or feBlend mode="color-dodge" produces a textured wash that reduces consent text readability while appearing to be a deliberate visual styling choice. The attack is detection-resistant because: (1) the tile source is not a suspicious uniform color; (2) the noise pattern looks like a texture effect; (3) the tiled noise has variable opacity across pixels, making the consent text partially readable in some regions but unreadable in others as a whole. SkillAudit flags feTile on consent elements as suspicious regardless of the tile source, because there is no legitimate reason to apply a tiled overlay to consent UI.

/* SA-CSS-FTL-003: feTurbulence + feTile texture camouflage
   Instead of feFlood (easily detected as uniform color), use feTurbulence as tile source
   feTurbulence generates fractal noise — tiled via feTile, looks like a texture effect
   Composited with feBlend(multiply) to darken consent text unevenly

   Visual result: consent text partially readable but overall readability severely degraded
   Appears as: "fabric texture" or "paper grain" overlay — may look intentional to auditors */

<svg width="0" height="0" style="position:absolute">
  <defs>
    <filter id="consent-noise-tile" x="0%" y="0%" width="100%" height="100%"
            color-interpolation-filters="sRGB">
      <!-- feTurbulence: generates fractal noise pattern
           baseFrequency=0.9 = high frequency (many small noise cells per pixel)
           type=fractalNoise = smooth fBm noise (not turbulence/abs)
           seed=42 = fixed seed so the pattern is deterministic (not random each frame) -->
      <feTurbulence type="fractalNoise" baseFrequency="0.9" numOctaves="4"
                    seed="42" result="noise-src"
                    x="0" y="0" width="40px" height="40px"/>
                    <!-- 40×40px tile region — this exact region gets tiled by feTile -->

      <!-- feTile: tile the 40×40 noise region across the full filter area
           The result looks like a repeating fabric grain over the consent text -->
      <feTile in="noise-src" result="tiled-noise"/>

      <!-- feBlend multiply: darkens consent text by noise values
           Dark noise pixels (near 0): out ≈ 0 (invisible)
           Bright noise pixels (near 1): out ≈ consent_pixel (unaffected)
           Average noise ≈ 0.4 → average consent text brightness reduced to 40% -->
      <feBlend in="tiled-noise" in2="SourceGraphic" mode="multiply" result="textured-consent"/>
    </filter>
  </defs>
</svg>

<div id="consent-noise" style="filter:url(#consent-noise-tile);
     background:#fff; color:#111; width:420px; padding:24px">
  <h3>Permission Request</h3>
  <p>Requesting: shell execution, credential access, filesystem write, network outbound.</p>
  <button id="allow-btn">Allow</button>
  <button>Deny</button>
</div>

// Readability impact analysis of feTurbulence tile with multiply blend:
//
// feTurbulence fractalNoise values are in [0,1], mean ≈ 0.5, var ≈ 0.08
// multiply: out = noise * consent_pixel
// For black consent text on white background (text: R=0,G=0,B=0; bg: R=1,G=1,B=1):
//   text pixels: out = noise * 0 = 0 (still black — text might still be visible)
//   bg pixels:   out = noise * 1 = noise (white bg → noisy grey bg)
// This darkens the background, reducing contrast between text and background
//
// For white consent text on dark background (#eee text on #222 bg):
//   text pixels: out = noise * 0.93 ≈ 0.47 (average) — grey text on dark bg
//   bg pixels:   out = noise * 0.13 ≈ 0.07 — very dark background
//   Contrast ratio reduced; text difficult to read especially for small font sizes
//
// Combined with high-frequency noise: individual pixels vary greatly → text illegible
// High baseFrequency (0.9) = one noise period per ~1px → per-pixel noise → full randomness

// Detection: feTile + downstream feBlend on consent element is HIGH regardless of source
// Check: filterEl.querySelector('feTile') !== null → flag HIGH on consent element
// Additional check: feTurbulence upstream of feTile → detection still applies (not lowered)
// Rationale: feTile has no legitimate use in consent UI rendering

HIGH — SA-CSS-FTL-003: feTurbulence → feTile → feBlend mode="multiply" applies a tiled noise texture over the consent text that visually resembles a fabric grain or paper texture effect. The multiply blend reduces average consent text brightness to approximately 40% of its original value (noise mean ≈ 0.5 × text pixel), with high per-pixel variance making the text difficult to read holistically. The attack is detection-resistant to auditors who look for uniform-color tile sources, because feTurbulence does not produce a suspicious flat color. Detection requires flagging any feTile in the filter graph of a consent element, regardless of what upstream primitive supplies the tile image.

Attack 4: Detection resistance — feTile as an intermediate step evading feFlood-focused rules (SA-CSS-FTL-004)

Security tools and manual auditors who have learned to check for feFlood → feBlend(normal) as a direct consent-covering attack pattern may miss the feFlood → feTile → feBlend(normal) variant. The feTile intermediate step breaks the direct pairing; detection rules that match feFlood result="X" immediately followed by feBlend in="X" do not fire when the chain is feFlood result="X" → feTile in="X" result="Y" → feBlend in="Y". The feBlend’s in attribute references the feTile result (Y), not the feFlood result (X).

The robust defence against this evasion requires tracing the filter primitive data-flow graph rather than checking for specific primitive-pair patterns. The graph traversal starts from the final output primitive, follows each in and in2 reference backward through the graph, and flags any feFlood, feImage, or other suspicious primitives that are reachable via any path from the final composite. This graph traversal correctly identifies the chain feFlood → feTile → feBlend because feFlood is reachable from feBlend via feTile. The same traversal also handles more deeply nested chains: feFlood → feTile → feColorMatrix → feBlend.

/* SA-CSS-FTL-004: Detection resistance — feTile as intermediate step breaks naive pattern matching
   Naive rule: look for feFlood immediately connected to feBlend
   Bypassed by: feFlood → feTile → feBlend (feBlend.in = feTile.result, not feFlood.result)

   Robust detection: graph traversal from final output backward to source primitives */

// Naive (fragile) detection — misses feTile intermediate:
function naiveDetect(filterEl) {
  for (const blend of filterEl.querySelectorAll('feBlend')) {
    const in1  = blend.getAttribute('in') || '';
    const mode = blend.getAttribute('mode') || 'normal';
    // Looks for feFlood directly referenced in feBlend.in:
    const directFlood = filterEl.querySelector(`feFlood[result="${in1}"]`);
    if (directFlood && mode === 'normal') {
      return 'CRITICAL'; // ← Correctly catches feFlood → feBlend(normal)
    }
  }
  return null; // ← MISSES feFlood → feTile → feBlend(normal)
}

// Robust detection — graph traversal:
function traverseBack(filterEl, resultName, visited = new Set()) {
  if (visited.has(resultName)) return [];
  visited.add(resultName);

  const primitive = filterEl.querySelector(`[result="${resultName}"]`);
  if (!primitive) return [];

  const tag = primitive.tagName.toLowerCase();
  const suspicious = [];

  // Flag suspicious source primitives
  if (tag === 'feflood') {
    const opacity = parseFloat(primitive.getAttribute('flood-opacity') || '1');
    suspicious.push({
      type: 'feFlood',
      floodColor: primitive.getAttribute('flood-color') || 'black',
      floodOpacity: opacity,
      severity: opacity >= 0.5 ? 'CRITICAL' : 'HIGH'
    });
  } else if (tag === 'feimage') {
    suspicious.push({ type: 'feImage', href: primitive.getAttribute('href') || '', severity: 'CRITICAL' });
  }

  // Recurse into upstream primitives
  const in1 = primitive.getAttribute('in') || '';
  const in2 = primitive.getAttribute('in2') || '';
  for (const upstream of [in1, in2]) {
    if (upstream && upstream !== 'SourceGraphic' && upstream !== 'SourceAlpha') {
      suspicious.push(...traverseBack(filterEl, upstream, visited));
    }
  }

  return suspicious;
}

function robustDetect(filterEl) {
  const findings = [];
  // Start from all leaf primitives (those whose result is not referenced by any in/in2)
  // For simplicity, start from the last primitive in document order:
  const allPrimitives = filterEl.querySelectorAll('feBlend, feComposite, feMerge');
  for (const prim of allPrimitives) {
    // Trace back the upstream graph
    const in1 = prim.getAttribute('in')  || '';
    const in2 = prim.getAttribute('in2') || '';
    for (const upstream of [in1, in2]) {
      findings.push(...traverseBack(filterEl, upstream));
    }
  }
  return findings;
}

// robustDetect(filterEl) for feFlood → feTile → feBlend chain:
// → [{
//     type: "feFlood",
//     floodColor: "white",
//     floodOpacity: 1,
//     severity: "CRITICAL"
//   }]
// (Traversal: feBlend.in="tiled-white" → feTile[result="tiled-white"].in="white-tile-src"
//  → feFlood[result="white-tile-src"] → FOUND)

// The graph traversal correctly identifies feFlood through any number of intermediate
// feTile, feColorMatrix, or other pass-through primitives

MEDIUM — SA-CSS-FTL-004: Inserting feTile between feFlood and feBlend breaks naive detection rules that look for directly connected primitive pairs. The filter graph must be traversed backward from the final composite primitive, following all in and in2 references, to find suspicious upstream sources (feFlood, feImage) regardless of how many intermediate primitives separate them. The robust traversal correctly identifies the chain feFlood → feTile → feBlend and any more deeply nested variants. Detection tools that only check for specific primitive-pair patterns without graph traversal are systematically evaded by the feTile intermediate step.

Summary table

AttackMechanismWhat it hidesSeverity
SA-CSS-FTL-001: feFlood → feTile → feBlend(normal) feFlood generates a uniform-color 1px tile; feTile replicates across filter region; feBlend(normal) composites on top; initial flood-opacity=0 (no-op) passes pre-render checks; attribute mutation to flood-opacity=1 activates the opaque covering attack All consent text when flood-opacity=1 (active state); transparent no-op in initial state (flood-opacity=0); covers entire consent panel with flood-color; DOM structure intact; textContent intact High
SA-CSS-FTL-002: feFlood → feTile → feBlend(multiply) feFlood generates dark-grey or black tile; feTile replicates; feBlend(multiply) = tile*consent_pixel; black tile → zero output; dark grey tile → consent text reduced to near-black; multiply formula guarantees all bright (readable) content is suppressed proportionally All consent text (black tile: complete suppression); lighter shades of text (dark-grey tile: reduces to near-black); most effective for bright-on-dark consent panels; multiply attack preserves element structure while making text unreadable High
SA-CSS-FTL-003: feTurbulence → feTile → feBlend(multiply) feTurbulence noise as tile source; looks like texture/fabric effect; multiply with average noise ≈ 0.4 reduces consent text to 40% brightness; high-frequency noise (baseFrequency=0.9) creates per-pixel variance making text holistically unreadable; detection-resistant vs. feFlood-focused rules Consent text readability degraded by noise multiply; appears as deliberate texture styling; no single pixel is completely covered but holistic readability (words, permissions, sentences) severely impaired; harder to detect than uniform-color tile source High
SA-CSS-FTL-004: feTile intermediate evasion feTile inserted between feFlood and feBlend breaks primitive-pair detection rules; feBlend.in references feTile result, not feFlood result directly; naive rules miss the chain; robust graph traversal follows in/in2 references backward through all intermediaries The detection mechanism — all FTL-001 through FTL-003 attacks evade naive primitive-pair rules via feTile insertion; robust detection requires backward graph traversal from all composite primitives following every in/in2 reference chain Medium

Defences

SkillAudit findings for this attack surface

HIGH SA-CSS-FTL-001: feFlood → feTile → feBlend mode="normal" on consent element — three-primitive chain constructing a transparent overlay (flood-opacity=0 initial state); single attribute mutation to flood-opacity=1 activates opaque white covering; passes pre-render security checks in initial state; detection requires checking feTile source primitive regardless of current flood-opacity value.
HIGH SA-CSS-FTL-002: feFlood → feTile → feBlend mode="multiply" on consent element — dark-grey or black flood tiled uniformly; multiply formula reduces consent text to near-black output; black flood produces absolute zero (all text invisible); dark-grey flood produces near-black (unreadable against dark background); detection requires checking downstream feBlend mode in feTile graph.
HIGH SA-CSS-FTL-003: feTurbulence → feTile → feBlend mode="multiply" on consent element — noise tile visually resembles texture/fabric effect; multiply blend reduces average consent text brightness to ~40%; high-frequency noise makes text holistically unreadable; detection-resistant against feFlood-focused rules; detection requires flagging feTile regardless of tile source.
MEDIUM SA-CSS-FTL-004: feTile intermediate evasion — inserting feTile between feFlood and feBlend breaks primitive-pair detection rules (feBlend.in = feTile result, not feFlood result); robust detection requires backward graph traversal following all in/in2 references; naive rules are systematically evaded; graph traversal traverses feFlood → feTile → feBlend correctly.

Related: feImage consent attacks  |  feMerge consent attacks  |  feBlend blending mode consent attacks

← Blog  |  Security Checklist