MCP server SVG feBlend lighten/screen consent security
Four SA-SVG-FBL consent attack patterns using SVG feBlend with light-emitting blend modes and white input sources to erase consent text. The SVG feBlend primitive is the SVG equivalent of CSS mix-blend-mode, but it operates within filter pipelines and takes explicit input images rather than blending with the page backdrop. By feeding a white feFlood output as the second input to feBlend mode="lighten" or feBlend mode="screen", attackers exploit the mathematical identity of these modes with a white source to produce fully white output regardless of the original text color.
The SVG feBlend primitive composites two images using a specified blend mode, applying the same per-channel formulas as CSS mix-blend-mode. The critical difference is that feBlend works with explicit named inputs within a filter pipeline rather than with the page backdrop, giving attackers direct control over what the “second image” is. By constructing a white image via feFlood flood-color="white" and using it as an input to feBlend mode="lighten", the attacker exploits a mathematical property: lighten(c, 1.0) = max(c, 1.0) = 1.0 for any channel value c in the normalized [0,1] range. Every channel of every pixel in the output is forced to 1.0 (white), regardless of the original text color or alpha.
SA-SVG-FBL-001: feBlend mode=lighten with white feFlood
The lighten blend mode computes the maximum channel value between two inputs at each pixel: max(in, in2) per R, G, B channel. When in2 is a fully opaque white image from feFlood flood-color="white" flood-opacity="1", the maximum is always 1.0 (white) at every channel. Dark text pixels with value 0 (black) become max(0, 1) = 1 (white). Any colored text pixel becomes max(colored_value, 1) = 1. The entire filter output region is white. This is the SVG analog of CSS mix-blend-mode: lighten against a white backdrop, but embedded in a filter pipeline where the white source is explicitly controlled.
<svg xmlns="http://www.w3.org/2000/svg">
<defs>
<filter id="lighten-erase">
<feFlood flood-color="white" flood-opacity="1" result="white-layer"/>
<feBlend in="SourceGraphic" in2="white-layer" mode="lighten"/>
</filter>
</defs>
</svg>
<p class="consent-text" style="filter:url(#lighten-erase)">
By clicking Allow you grant this MCP server read access to your file system.
</p>
<!-- feBlend mode=lighten formula (per channel, normalized 0..1):
output_channel = max(in_channel, in2_channel)
For black text pixel (R=0, G=0, B=0) with white in2 (R=1, G=1, B=1):
R_out = max(0, 1) = 1 (white)
G_out = max(0, 1) = 1 (white)
B_out = max(0, 1) = 1 (white)
→ Glyph pixel becomes white
For any other text color (R=c, G=c, B=c):
R_out = max(c, 1) = 1 (always white when in2=1.0)
→ Color doesn't matter; any text becomes white
For background pixel (transparent in SourceGraphic):
Alpha handling in feBlend: output_alpha = 1 - (1-alpha_in)*(1-alpha_in2)
= 1 - (1-0)*(1-1) = 1 - 1*0 = 1
→ Background pixels also become opaque white
→ Entire filter region becomes a white rectangle
Note on input order:
feBlend in="SourceGraphic" in2="white-layer": SourceGraphic is "in", white is "in2"
feBlend in="white-layer" in2="SourceGraphic": reversed order
For lighten: max is commutative → same result regardless of input order
For screen: also commutative (1-(1-A)(1-B) = 1-(1-B)(1-A)) → same result
Attackers may swap order to evade detection checks for specific input order
CSS comparison:
CSS mix-blend-mode:lighten blends an element with the page backdrop.
SVG feBlend creates explicit two-image blend within filter pipeline.
Both exploit the same max() formula but in different rendering contexts.
Detection:
1. Find elements with filter:url(#id) applied
2. Parse the referenced SVG filter
3. Check for feBlend with mode="lighten" or mode="screen"
4. Check if any input to feBlend is a feFlood with high brightness flood-color
5. Flag as SA-SVG-FBL-001 if feFlood brightness > 0.85 and mode is lighten/screen -->
SVG feBlend vs CSS mix-blend-mode: While CSS mix-blend-mode blends an element with the page backdrop (requiring the page to be white for the attack to work), SVG feBlend operates with explicit inputs within the filter. The attacker controls both inputs: in="SourceGraphic" (the original text) and in2 (a synthesized white image from feFlood). This makes SVG feBlend attacks independent of the page background color and effective on any background — including dark-mode pages where CSS mix-blend-mode:lighten would not produce white output.
feBlend mode="lighten" with white feFlood input: max(text_channel, 1.0) = 1.0 at every pixel; all text becomes white; effective on any page background (not just white). Detection: feBlend mode=lighten + feFlood in2 with flood-color luminance > 0.85 → CRITICAL.SA-SVG-FBL-002: feBlend mode=screen with white input
The screen blend mode computes 1 - (1 - A) × (1 - B) per channel. When B = 1.0 (white), the formula simplifies to 1 - (1 - A) × 0 = 1 regardless of A. Like lighten, screen with a white input unconditionally produces white at every channel. The practical difference is that the screen formula with non-white inputs produces brighter, more washed-out output than lighten, while lighten clips directly to the maximum. Both modes with a white input converge on the same result — total erasure — but screen is sometimes used instead of lighten to distinguish from patterns that specifically check for mode="lighten".
<filter id="screen-erase">
<feFlood flood-color="white" flood-opacity="1" result="white-src"/>
<feBlend in="SourceGraphic" in2="white-src" mode="screen"/>
</filter>
<!-- feBlend mode=screen formula (per channel):
output_channel = 1 - (1 - A) * (1 - B)
With B = 1.0 (white feFlood):
output_channel = 1 - (1 - A) * (1 - 1.0)
= 1 - (1 - A) * 0
= 1 - 0
= 1.0 (white)
This holds for any value of A (any text color, any opacity).
Comparison to CSS mix-blend-mode:screen:
CSS screen blends element with page backdrop.
When backdrop is white (typical white-background page):
Same formula applies: backdrop B=1 → output=1 for all text pixels.
SVG feBlend screen: same formula, but with explicit white feFlood instead of
relying on page background being white.
→ SVG version is page-background-independent.
Historical note:
The screen blend mode was designed for "double exposure" light combinations.
Its mathematical identity with a white input (always produces white) is
not accidental — it models the physics: shining a white light on any surface
reflects white, regardless of the surface color.
Evasion: attacker might use mode="color-dodge" or mode="hard-light" with
appropriately chosen feFlood values to achieve similar erasure with less
commonly checked mode values.
Combined feBlend mode coverage to check:
- lighten: max(A,B) → white with B=1
- screen: 1-(1-A)(1-B) → white with B=1
- color-dodge: A/(1-B) → white with B near 1 (approaches infinity, clips to white)
- overlay with B=1: same as screen for A<0.5 and lighten for A>0.5 → always white
- hard-light with A=1: same as screen → white
Correct detection must cover all these modes, not just lighten and screen. -->
Mode enumeration for detection: Scanners that check only for mode="lighten" and mode="screen" miss attacks using mode="color-dodge", mode="overlay", and mode="hard-light" with appropriately chosen input values. Correct detection should enumerate all SVG feBlend modes and compute the expected output luminance given the actual input values, then flag any combination that produces output below the WCAG contrast threshold.
feBlend mode="screen" with white feFlood input: 1 - (1-A) × 0 = 1 at every channel; all text becomes white; evasion via mode=screen when scanners check only mode=lighten. Detection: enumerate lighten, screen, color-dodge, overlay, hard-light + feFlood near-white input → CRITICAL.SA-SVG-FBL-003: feBlend mode=multiply with white input (identity — no attack)
Understanding what does not constitute an attack is equally important for correct detection. The multiply blend mode computes A × B per channel. When B = 1.0 (white), the formula is A × 1.0 = A — the identity transformation. A feFlood white input to feBlend mode=multiply has zero effect on the source graphic. Scanners that flag any feBlend with a feFlood input will produce false positives for this combination. Attackers may intentionally include feBlend mode="multiply" with a white feFlood as an innocuous-looking baseline to seed false-positive fatigue in reviewers.
<filter id="multiply-noop">
<feFlood flood-color="white" flood-opacity="1" result="white-src"/>
<feBlend in="SourceGraphic" in2="white-src" mode="multiply"/>
<!-- This filter has zero visible effect. -->
</filter>
<!-- feBlend mode=multiply formula:
output_channel = A * B
With B = 1.0 (white):
output_channel = A * 1.0 = A
The filter is mathematically equivalent to no filter at all.
Text renders identically to without the filter.
Why document this?
Scanners that generically flag "feBlend + feFlood" without computing
the actual blend output will produce false positives here.
This creates scanner noise that attackers can exploit to mask real attacks
among many multiply-noop patterns.
The correct check is mode-specific:
1. lighten + near-white in2: max(A, ~1) → white → ATTACK
2. screen + near-white in2: 1-(1-A)(~0) → ~1 → ATTACK
3. multiply + near-white in2: A*~1 → ~A → NO ATTACK (near identity)
4. color-burn + near-white in2: 1-(1-A)/B → with B~1: 1-(1-A)/1 = A → NO ATTACK
5. darken + near-white in2: min(A, ~1) → A (text is darker than white) → NO ATTACK
6. color-dodge + near-white in2: A/(1-B) → with B→1: A/0 → ∞ → clips to 1 → ATTACK
Mode classification:
- Lightening modes (lighten, screen, color-dodge, hard-light high): ATTACK with white in2
- Darkening modes (darken, multiply, color-burn): NO ATTACK with white in2
(white makes no text darker)
- Contrast modes (overlay, hard-light low): depends on exact values
Build a mode lookup table for correct scanner behavior. -->
Mode-specific expected output: Build a lookup table mapping (mode, in2_luminance) pairs to expected output luminance. Lightening modes with high in2 luminance are attacks; darkening modes with white in2 are not. A scanner that flags all feBlend with feFlood generates excessive false positives. The correct approach is computing the blend formula output for representative pixel values and checking whether the result falls below the WCAG contrast threshold.
feBlend mode="multiply" with white input: A × 1.0 = A — identity, no visual effect; NOT an attack. Scanners should not flag this. Darkening modes (darken, multiply, color-burn) with white input produce near-identity output and should be excluded from attack detection to prevent false positives.SA-SVG-FBL-004: near-white feFlood to feBlend lighten for keyword evasion
Using a near-white but not exactly white flood color as the feBlend input evades scanners that check flood-color against the keyword "white" or the hex value "#ffffff". A flood-color of #fdfdfd (R=253, G=253, B=253, normalized ~0.992) fed into feBlend mode="lighten" produces max(A, 0.992). For any text color with channel values below 0.992 (the vast majority — even very light gray text), the output is 0.992. The WCAG contrast between rgb(253,253,253) and the white page background rgb(255,255,255) is approximately 1.003:1 — effectively invisible.
<filter id="near-white-lighten">
<feFlood flood-color="#fdfdfd" flood-opacity="1" result="near-white"/>
<feBlend in="SourceGraphic" in2="near-white" mode="lighten"/>
</filter>
<!-- Near-white flood-color calculation:
flood-color="#fdfdfd": R=253, G=253, B=253 (normalized: 0.992)
feBlend mode=lighten: max(A, 0.992)
For any text pixel with channel value A ≤ 0.992 (i.e., any non-pure-white text):
output = 0.992
WCAG contrast between rgb(253,253,253) and white page background rgb(255,255,255):
Luminance of rgb(253,253,253): ≈ 0.984 (very close to 1.0)
Luminance of rgb(255,255,255): = 1.0
Contrast ratio = (1.0 + 0.05) / (0.984 + 0.05) ≈ 1.003:1
This is essentially invisible — the text appears as the same color as the background.
Evasion variants:
- flood-color="#fefefe" (R=254): even closer to white, contrast ≈ 1.001:1
- flood-color="#f8f8f8" (R=248): slightly more visible but still fails WCAG at ~1.02:1
- flood-color as RGB function: flood-color="rgb(253,253,253)" — avoids hex check
- flood-color as HSL: flood-color="hsl(0,0%,99%)" — avoids hex/rgb checks
Detection requires normalizing flood-color regardless of representation:
1. Parse flood-color keyword / hex / rgb() / hsl() → normalized RGB
2. Compute luminance of flood color
3. Compute luminance of effective page/element background
4. If |flood_luminance - background_luminance| < threshold for lighten/screen mode
→ flag as near-background erasure attack
Threshold recommendation: contrast ratio < 2.0:1 between flood output and background
→ SA-SVG-FBL-004 HIGH -->
Normalize all color representations: SVG flood-color accepts keywords, hex values, rgb(), hsl(), and other CSS color formats. A scanner that checks only for the keyword "white" or the exact hex "#ffffff" will miss "#fefefe", "rgb(254,254,254)", and "hsl(0,0%,99.6%)". Detection must parse and normalize the flood-color to a luminance value, then compute the expected contrast ratio after blending, regardless of how the color was specified.
feFlood flood-color="#fdfdfd" to feBlend mode="lighten": output ~0.992 per channel; contrast ≈ 1.003:1 against white background; evades keyword-based flood-color="white" detection. Detection: normalize flood-color to luminance; check WCAG contrast of blended output against background → HIGH if below 2.0:1.Summary of SA-SVG-FBL findings
| ID | Severity | Pattern | Formula | With white in2 |
|---|---|---|---|---|
| SA-SVG-FBL-001 | CRITICAL | feBlend mode=lighten + white feFlood | max(A, B) | max(A, 1) = 1 always → white |
| SA-SVG-FBL-002 | CRITICAL | feBlend mode=screen + white feFlood | 1-(1-A)(1-B) | 1-(1-A)×0 = 1 always → white |
| SA-SVG-FBL-003 | INFO | feBlend mode=multiply + white feFlood (identity) | A×B | A×1 = A → no effect; NOT an attack |
| SA-SVG-FBL-004 | HIGH | Near-white flood-color (#fdfdfd) to lighten | max(A, 0.992) | Contrast ~1.003:1 → invisible; keyword-evasion |