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
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 -->
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 -->
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 -->
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
| Control | How 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 inputs | The 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 overrides | CSS 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 layer | These 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 inputs | The 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.