← Blog

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.

By SkillAudit  |  24 Oct 2026

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:

ModeComponent from in1Components from in2Achromatic in2 resultAttack?
hueHS, LOutput at L(in2), S=0Yes — L-collapse
saturationSH, LOutput at L(in2), H=0Yes — L-collapse
colorH, SLOutput at L(in2)Yes — L-collapse
luminosityLH, SOutput at L(source) on achromatic in2Weaker — 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

IDModein2 HSLOutput LEffectSeverity
SA-CSS-SAT-001saturationS=0, L=1 (white)1Achromatic white-outCritical
SA-CSS-SAT-002saturationS=0, L=0 (black)0Achromatic dark-outCritical
SA-CSS-SAT-003saturationS=0, L=0.5 (gray)0.5Contrast collapse; WCAG ~3.95:1High
SA-CSS-SAT-004saturation (mousedown)0→1 opacity, white0→1Click-time white-outCritical
SA-CSS-SAT-005saturation/hue comparisonachromatic any LL(in2)Identical attack via different code pathHigh

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-SAT-001: 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.
CRITICAL SA-CSS-SAT-002: 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).
HIGH SA-CSS-SAT-003: 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.
CRITICAL SA-CSS-SAT-004: JS interaction handler sets flood-opacity 0→1 with white feFlood on mode=saturation — click-time: output L=1; white-out; MutationObserver on feFlood opacity changes in saturation-mode filter chains within consent ancestors required.

Related: Unified HSL feBlend detection (hue, saturation, color, luminosity)  |  feBlend mode=hue attacks  |  feBlend mode=color attacks

← Blog  |  Security Checklist