MCP server CSS feBlend security: SVG feBlend mode="screen" white flood consent erase, mode="multiply" black flood, color-dodge near-white layer, and luminosity blend consent text hide

Published 2026-09-26 — SkillAudit Research

The SVG feBlend filter primitive applies a blend mode formula between two input images at each pixel position. The set of supported modes includes normal, multiply, screen, overlay, darken, lighten, color-dodge, color-burn, hard-light, soft-light, difference, exclusion, and the non-separable modes hue, saturation, color, and luminosity. Each mode has deterministic mathematical properties that can be exploited when one of the blend inputs is an attacker-controlled feFlood color.

The screen mode identity — screen(white, any) = white — makes it trivially easy to erase any consent text with a white feFlood input. The multiply mode has the symmetric dark-mode property — multiply(black, any) = black. The color-dodge mode's near-singularity as the dodge layer approaches white bleaches dark pixels to near-white regardless of input value. The luminosity non-separable mode replaces the source's luminosity channel with the backdrop's, converting dark text to a bright variant when blended with a white backdrop. In all these attacks, the consent element's fill attribute, computed style, dimensions, and DOM presence are unaffected — only the filter output pixels are manipulated.

Mathematical guarantee: The screen-with-white and multiply-with-black attacks are not probabilistic — they are identities guaranteed by the blend mode formulas. No matter what color the consent text is, screen(white, text) = white exactly. The only detection method is simulating the blend mode output using the actual flood color inputs from the filter graph.

Attack findings

CRITICAL
feBlend mode="screen" with white flood — screen(white, any) = white, consent text pixels erased to background
feBlend with mode="screen" computes: result = 1 - (1-a)(1-b) per channel, where a and b are the two input pixel values (normalized 0–1). When one input is white (all channels = 1.0), screen(1.0, any) = 1 - (1-1)(1-any) = 1 regardless of the other input. A white feFlood composited as one input of feBlend mode="screen" produces white output regardless of consent text color or opacity. The text element is present in the DOM; all standard checks pass; the SVG filter graph is the attack path.
<filter id="consent-screen-erase">
  <!-- White flood: all channels = 1.0 -->
  <feFlood flood-color="#ffffff" flood-opacity="1" result="white-flood"/>
  <!-- screen(white, any) = white — consent text erased to background -->
  <feBlend in="white-flood" in2="SourceGraphic" mode="screen" result="out"/>
</filter>
<!--
  For any consent text pixel:
  screen(1.0, text_channel) = 1 - (1-1)*(1-text_channel) = 1.0

  Output: solid white rectangle over consent text area
  Renders identical to background; text is invisible

  All DOM/CSS checks pass:
  fill, opacity, display, visibility, dimensions → normal
  Only filter output simulation detects attack -->
HIGH
feBlend mode="multiply" with black flood composited on black background — multiply(black, any) = black, invisible on black background
feBlend with mode="multiply" computes: result = a × b per channel. When one input is black (all channels = 0), multiply(0, any) = 0 regardless of the other input. A black feFlood as one input of feBlend mode="multiply" produces black output regardless of consent text. On a dark or black background (common in dark-mode UIs), the text renders as black-on-black. This is the dark-mode variant of the screen attack.
<filter id="consent-multiply-erase">
  <!-- Black flood on dark-mode UI -->
  <feFlood flood-color="#000000" flood-opacity="1" result="black-flood"/>
  <!-- multiply(0, any) = 0 — consent text erased to background -->
  <feBlend in="black-flood" in2="SourceGraphic" mode="multiply"/>
</filter>
<!-- Dark-mode background: #0a0a0a
     multiply(0, text_channel) = 0 → output #000000
     Contrast vs #0a0a0a background: ~1:1 → invisible

     Dark-mode environments where background is near-black are particularly vulnerable:
     any consent text with multiply(flood, text) where flood approaches background color
     produces zero-contrast output -->
MEDIUM
feBlend mode="color-dodge" with near-white layer bleaches dark consent text pixels
feBlend with mode="color-dodge" computes: result = a / (1 - b) where b is the dodge layer. When b approaches 1.0 (near-white), the denominator (1-b) approaches 0, and result approaches infinity — clamped to 1.0 (white). A near-white feFlood (flood-color="#f5f5f5", ~0.96 normalized) dodges dark consent text pixels to near-white. Because 1/(1-0.96) = 25, even dark pixels (e.g., 0.1) dodge to min(0.1×25, 1.0) = 1.0 (white). The dodge layer appears as a light highlight or "brightness" filter to a visual reviewer.
<filter id="consent-dodge">
  <!-- Near-white dodge layer: creates "brightness boost" appearance -->
  <feFlood flood-color="#f0f0f0" flood-opacity="0.95" result="dodge-layer"/>
  <!-- color-dodge bleaches dark pixels via near-infinity denominator -->
  <feBlend in="SourceGraphic" in2="dodge-layer" mode="color-dodge"/>
</filter>
<!-- Dark text pixel (normalized: 0.1) on near-white dodge (0.94 normalized):
     dodge = source / (1 - dodge_layer)
           = 0.1 / (1 - 0.94) = 0.1 / 0.06 = 1.67 → clamped to 1.0 (white)

     All dark consent text pixels bleached to near-white
     Visually appears as an overexposed highlight, not an attack -->
MEDIUM
feBlend mode="luminosity" with background-luminance layer replaces consent text luminosity with background luminosity
feBlend with mode="luminosity" replaces the luminosity of the source image with the luminosity of the backdrop. If the backdrop input has a luminosity matching the consent background (white → luminosity = 1.0), the output preserves the source's hue and saturation but replaces its luminosity with 1.0 (maximum brightness). Dark consent text (#2d2d2d, luminosity ≈ 0.11) outputs as a very bright version of that same gray. The hue is preserved; saturation is preserved; only luminosity changes — meaning the output appears as a near-white gray.
<filter id="consent-lum-replace">
  <!-- Backdrop input with luminosity = 1.0 (white) -->
  <feFlood flood-color="#ffffff" flood-opacity="1" result="white-backdrop"/>
  <!-- mode="luminosity": output = hue(source) + saturation(source) + luminosity(white) = near-white -->
  <feBlend in="SourceGraphic" in2="white-backdrop" mode="luminosity"/>
</filter>
<!-- mode="luminosity" formula (simplified):
     out_L = backdrop_L        = 1.0 (from white backdrop)
     out_H = source_H          = gray → H ≈ 0° (achromatic)
     out_S = source_S          = 0 (gray is unsaturated)

     Gray with luminosity=1.0 → white
     Colored text: hue and saturation preserved, but luminosity = 1.0 → very bright version
     Low-saturation consent text approaches white; contrast degrades significantly -->

Detection

function checkFeBlend(svgRoot) {
  const findings = [];
  const blends = svgRoot.querySelectorAll('feBlend');
  for (const blend of blends) {
    const mode = blend.getAttribute('mode') || 'normal';
    const filter = blend.closest('filter');
    const inRef = blend.getAttribute('in') || 'SourceGraphic';
    const in2Ref = blend.getAttribute('in2') || 'BackgroundImage';

    // Helper: resolve a result name to the feFlood element that produced it
    function resolveFlood(resultName) {
      return filter?.querySelector(`feFlood[result="${resultName}"]`) || null;
    }

    if (mode === 'screen') {
      // screen(white, any) = white — check if either input is near-white flood
      const flood1 = resolveFlood(inRef);
      const flood2 = resolveFlood(in2Ref);
      for (const flood of [flood1, flood2]) {
        if (!flood) continue;
        const color = getComputedStyle(flood).floodColor || flood.getAttribute('flood-color') || '';
        if (color.match(/#f[ef][ef][ef][ef][ef]|#fff|rgb\(25[0-9]|white/i)) {
          const opacity = parseFloat(getComputedStyle(flood).floodOpacity || flood.getAttribute('flood-opacity') || '1');
          if (opacity > 0.8) findings.push({ severity: 'critical', mode, issue: 'feBlend mode="screen" with near-white flood input → screen(white, text) = white; consent text erased' });
        }
      }
    }

    if (mode === 'multiply') {
      // multiply(black, any) = black — check if either input is near-black flood
      const flood1 = resolveFlood(inRef);
      if (flood1) {
        const color = getComputedStyle(flood1).floodColor || flood1.getAttribute('flood-color') || '';
        if (color.match(/#0[01234][01234]|#000|rgb\([0-9]{1,2},/)) {
          findings.push({ severity: 'high', mode, issue: 'feBlend mode="multiply" with near-black flood → multiply(black, text) = black; invisible on dark backgrounds' });
        }
      }
    }

    if (mode === 'color-dodge') {
      // color-dodge with near-white layer bleaches dark pixels
      const flood = resolveFlood(in2Ref);
      if (flood) {
        const opacity = parseFloat(getComputedStyle(flood).floodOpacity || flood.getAttribute('flood-opacity') || '1');
        if (opacity > 0.85) findings.push({ severity: 'medium', mode, issue: `feBlend mode="color-dodge" with near-white dodge layer (opacity ${opacity}) bleaches dark consent text to near-white` });
      }
    }
  }
  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
For feBlend on consent text, simulate blend mode output by applying the blend formula to the source pixel color with the actual flood color/opacity from the in/in2 inputsThe blend mode formula determines the rendered output color; the consent element's fill attribute is not the rendered color when feBlend is applied, so contrast measurements on fill alone are incorrect
Check both inputs of feBlend for feFlood primitives; compute effective flood color via getComputedStyle (not getAttribute) to detect CSS overridesCSS can override flood-color and flood-opacity presentation attributes; using computed style ensures the actual browser-applied values are used in simulation rather than the SVG attribute values
Flag mode="screen" with any near-white input, mode="multiply" with any near-black input, and mode="color-dodge" with near-white dodge layerThese three mode-plus-color combinations have mathematical properties that guarantee near-total erasure of consent text regardless of the source text color; they warrant immediate escalation without needing full simulation
Require filter simulation in the effective color-interpolation-filters color space; linearRGB blends produce lighter outputs than sRGB for the same inputsThe blend formula operates on either gamma-encoded sRGB values or gamma-decoded linearRGB values depending on color-interpolation-filters; simulating in the wrong color space produces inaccurate contrast predictions, particularly for mid-tone inputs

SkillAudit resolves feBlend input references to their source feFlood primitives, simulates the blend mode formula using actual flood colors and opacities (via computed style), and flags the mathematically guaranteed erase patterns — screen-with-white and multiply-with-black — as immediate critical findings. Run a free audit on any MCP server GitHub URL to detect feBlend consent text attacks and the full SVG filter attack surface.