MCP server SVG feBlend saturation consent security
Five SA-CSS-SAT consent attack patterns using feBlend mode="saturation" — the HSL-decomposition mode that takes the saturation component from the source graphic and the hue and lightness from in2, producing achromatic white-out when in2 is white (L=1), dark-out when in2 is black (L=0), and mid-luminance contrast collapse when in2 is achromatic gray — identical attack effects to mode=hue for achromatic in2 despite different component assignments.
The feBlend SVG filter primitive's mode="saturation" is a member of the HSL-decomposition family alongside hue, color, and luminosity modes. Where mode=hue takes the Hue component from in1 and the Saturation and Lightness from in2, mode=saturation reverses the saturation component: it takes the Saturation from in1 (the source, Cs) and the Hue and Lightness from in2 (the backdrop, Cb).
For consent-text attacks with achromatic in2 (white, black, gray), the saturation mode produces the same L-collapse as the hue mode: since achromatic colors have S=0 and a well-defined L, the output H=H(in2)=0, L=L(in2), S=S(in1)=source-saturation. But S-nonzero output with achromatic hue (H=0) is still achromatic — a color with H=0 (red hue angle) at S=0.8 and L=0.5 is achromatic gray, not red, because S only matters when L is not at the extremes and when H defines a non-gray direction in color space. In practice, for achromatic in2 the output luminance L(in2) fully determines readability, identically to mode=hue.
Saturation mode with achromatic in2 = hue mode with achromatic in2: Both modes take L from in2. For achromatic in2 (S=0), both produce output at L=L(in2). White in2 → white output. Black in2 → black output. Gray in2 → gray output. The attack effect is perceptually identical despite different HSL component assignments. Separate detection entries are still required because mode strings differ.
HSL decomposition: how mode=saturation works
/* mode=saturation: compose color from two HSL decompositions */ HSL(in1) = (H_source, S_source, L_source) /* source: consent text */ HSL(in2) = (H_blend, S_blend, L_blend) /* backdrop: feFlood */ output_HSL = (H_blend, S_source, L_blend) /* H+L from backdrop, S from source */ /* Attack cases with achromatic in2 */ in2=white → HSL(white) = (*, 0, 1) → output(0, S_src, 1) at L=1 → white (L=1 dominates) in2=black → HSL(black) = (*, 0, 0) → output(0, S_src, 0) at L=0 → black (L=0 dominates) in2=gray50 → HSL(gray) = (*, 0, 0.5) → output(0, S_src, 0.5) → gray at L=0.5 /* Note: output H = H(in2); for achromatic in2 H is undefined/0 */ /* output S = S(in1) = S(source) — this is what "saturation mode" takes from source */ /* output L = L(in2) — lightness from backdrop controls visibility */ /* For chromatic in2 the attack is weaker since H≠0 and non-zero S+L may render colorfully */ /* Achromatic in2 (S=0, H=undefined) forces achromatic output by nullifying color */
SA-CSS-SAT-001: feBlend mode="saturation" + white in2 (L=1, S=0) — achromatic white-out
A feFlood with flood-color="white" has HSL = (undefined, 0, 1). Mode=saturation takes H=0 (white/undefined) and L=1 from in2, S from in1 (consent text). Output H=0, L=1, S=S(text). At L=1, regardless of S, the output is maximum lightness — achromatic white. (At L=1 and L=0 in HSL, S has no effect; the color is purely determined by L.) The consent text becomes white regardless of its original saturation value.
<filter id="sat001"> <feFlood flood-color="white" flood-opacity="1" result="whiteFlood"/> <feBlend in="SourceGraphic" in2="whiteFlood" mode="saturation"/> </filter> <!-- HSL(white) = (0, 0, 1); output H=0, S=S(text), L=1 --> <!-- At L=1, S has no perceptual effect; all output = white --> <!-- Dark text rgb(26,26,26): S≈0; output = white --> <!-- Saturated text rgb(30,60,200): H≈225°, S≈72%, L≈45%; output L=1 = white --> <!-- Any consent text color → white; identical attack to mode=hue+white --> <!-- Separate "saturation" detection entry required despite same visual effect -->
SA-CSS-SAT-002: feBlend mode="saturation" + black in2 (L=0, S=0) — achromatic dark-out
A feFlood with flood-color="black" has HSL = (undefined, 0, 0). Mode=saturation: output H=0, S=S(text), L=0. At L=0, S has no effect; all output is maximum darkness — black. On a dark-mode UI background, any consent text regardless of original color becomes black-on-black invisible.
<filter id="sat002"> <feFlood flood-color="black" flood-opacity="1" result="blackFlood"/> <feBlend in="SourceGraphic" in2="blackFlood" mode="saturation"/> </filter> <!-- HSL(black) = (0, 0, 0); output H=0, S=S(text), L=0 --> <!-- At L=0, S has no effect; all output = black --> <!-- Light text rgb(229,231,235): output = black; dark-out on dark UI -->
SA-CSS-SAT-003: feBlend mode="saturation" + achromatic gray in2 (L=0.5, S=0) — mid-luminance collapse
An achromatic gray feFlood has HSL = (undefined, 0, L_gray). Mode=saturation: output H=0, S=S(text), L=L_gray. The output luminance is controlled entirely by L_gray. At L_gray=0.5 (sRGB #808080), all consent text renders at L=0.5 — a mid-tone gray. The S(text) component produces a color with H=0 and S>0 at L=0.5, which would be a desaturated red. However, for dark gray consent text (S≈0), the output is also desaturated at L=0.5. WCAG: linearised L of sRGB 0.5 ≈ 0.216; (1.05)/(0.216+0.05)=3.95:1; fail.
<filter id="sat003"> <feFlood flood-color="#808080" flood-opacity="1" result="grayFlood"/> <feBlend in="SourceGraphic" in2="grayFlood" mode="saturation"/> </filter> <!-- HSL(#808080) = (0, 0, 0.502); output H=0, S=S(text), L=0.502 --> <!-- Dark desaturated text (S≈0): output gray at L=0.502; WCAG ≈3.95:1 vs white --> <!-- Note: if source text is chromatic (S>0) with H≠0: output H=0 (overwritten by in2) --> <!-- H from in2=gray is undefined/achromatic; output becomes achromatic at L=0.502 --> <!-- feFlood="#808080" ≠ "white" or "black"; evades endpoint string detection -->
SA-CSS-SAT-004: JS mousedown flood-color → white — click-time saturation white-out
Same interaction pattern as SA-CSS-HUE-004 but using mode=saturation. Initial state: flood-opacity="0". On mousedown: flood-color="white", flood-opacity="1". Mode=saturation with L(in2)=1 produces white output at click time.
<svg style="position:absolute;width:0;height:0">
<defs>
<filter id="sat004">
<feFlood id="sat004Flood" flood-color="white" flood-opacity="0" result="fl"/>
<feBlend in="SourceGraphic" in2="fl" mode="saturation"/>
</filter>
</defs>
</svg>
<div id="consent" style="filter:url(#sat004)">
I agree to grant full file-system access to this MCP server.
</div>
<script>
document.getElementById("consent").addEventListener("mousedown", () => {
document.getElementById("sat004Flood").setAttribute("flood-opacity", "1");
// saturation mode: output L=L(white)=1; white-out at click time
});
</script>
SA-CSS-SAT-005: mode=saturation vs mode=hue — achromatic convergence
For achromatic in2, the attack effect of mode=saturation is identical to mode=hue: both produce output at L=L(in2). This convergence occurs because:
- mode=hue: output = (H_source, S_blend, L_blend). For achromatic in2: S_blend=0, L_blend=L(in2). Output achromatic at L(in2).
- mode=saturation: output = (H_blend, S_source, L_blend). For achromatic in2: H_blend=0, L_blend=L(in2). Output at L(in2) with H=0. Non-zero S_source with H=0 in HSL would be red-hued, but achromatic in2 has undefined H so H=0 is arbitrary; the resulting color at L=L(in2) depends on color space implementation but renders near-achromatic in browsers.
| Mode | Component from in1 | Components from in2 | Achromatic in2 result | Attack? |
|---|---|---|---|---|
| hue | H | S, L | Output at L(in2), S=0 | Yes — L-collapse |
| saturation | S | H, L | Output at L(in2), H=0 | Yes — L-collapse |
| color | H, S | L | Output at L(in2) | Yes — L-collapse |
| luminosity | L | H, S | Output at L(source) on achromatic in2 | Weaker — preserves source L |
Mode=luminosity is the exception: it takes L from in1 (source) and H, S from in2. For achromatic in2, the output L = L(source text). This does not collapse contrast — dark text remains dark. The L-collapse attack applies to hue, saturation, and color modes but NOT to luminosity mode. Luminosity with non-achromatic in2 (chromatic feFlood) can still attack, but via a different mechanism.
Luminosity mode is different: Unlike hue, saturation, and color modes, luminosity mode takes L from the source (in1) — not from in2. Achromatic in2 with luminosity does not force L(in2) onto the output. The achromatic-in2 L-collapse attack covers only three of the four HSL modes: hue, saturation, and color. Luminosity requires separate analysis.
Attack summary
| ID | Mode | in2 HSL | Output L | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-SAT-001 | saturation | S=0, L=1 (white) | 1 | Achromatic white-out | Critical |
| SA-CSS-SAT-002 | saturation | S=0, L=0 (black) | 0 | Achromatic dark-out | Critical |
| SA-CSS-SAT-003 | saturation | S=0, L=0.5 (gray) | 0.5 | Contrast collapse; WCAG ~3.95:1 | High |
| SA-CSS-SAT-004 | saturation (mousedown) | 0→1 opacity, white | 0→1 | Click-time white-out | Critical |
| SA-CSS-SAT-005 | saturation/hue comparison | achromatic any L | L(in2) | Identical attack via different code path | High |
Defences
- Add saturation to the feBlend mode detection list: Alongside hue, color, and luminosity. The four HSL modes are distinct mode strings requiring four separate detection entries.
- HSL decomposition analysis for mode=saturation: When feBlend mode=saturation is detected in a consent ancestor filter graph, resolve in2 (feFlood) to L. The output L = L(in2). If L(in2) > 0.70, flag white-out (Critical). If L(in2) < 0.15, flag dark-out (Critical). If 0.30 < L(in2) < 0.70, flag contrast-collapse (High).
- Unified detection for hue, saturation, and color: All three modes share the L-collapse-from-in2 property with achromatic in2. A single L-based checker covers all three after mode identification. See the unified HSL detection page.
- Separate entries for luminosity: Mode=luminosity does not collapse via achromatic in2 because it takes L from in1 (source). Do not apply the same L(in2) check to luminosity — it produces false positives for achromatic feFlood with luminosity mode.
- Runtime MutationObserver: Monitor feFlood flood-color and flood-opacity changes within saturation-mode filter chains on consent ancestors. Any transition producing L(in2) > 0.70 or < 0.15 is a click-time attack.
SkillAudit findings for this attack surface
feBlend mode="saturation" with feFlood flood-color=white (L=1) — output L=1; achromatic white; consent text invisible on white/light background; identical attack effect to mode=hue+white and mode=color+white but via different HSL component assignment; separate "saturation" detection entry required.feBlend mode="saturation" with feFlood flood-color=black (L=0) — output L=0; achromatic black; dark-out on dark background; separate detection required from mode=hue+black (same effect, different mode string).feBlend mode="saturation" with achromatic gray feFlood (0.30<L<0.65) — output at L(in2) for all source hues and saturations; WCAG depends on L(in2); at sRGB 0.5 (≈L 0.216 linearised) WCAG vs white ≈3.95:1 (fail); evades endpoint flood-color string detection; threshold: L(in2) in [0.30, 0.65] triggers contrast simulation.