← Blog

MCP server SVG feBlend difference consent security

Five SA-CSS-DIF consent attack patterns using feBlend mode="difference" — the absolute-value subtraction formula that inverts dark consent text to near-white with a white blend layer, collapses contrast to a failing 2.06:1 with mid-gray, and triggers inversion at the instant of user interaction via a JS mousedown handler.

By SkillAudit  |  24 Oct 2026

The feBlend SVG filter primitive's mode="difference" computes the absolute value of the per-channel difference between the source graphic (Cs, in1) and the blend layer (Cb, in2). The formula is: out = |Cs − Cb| for each channel independently in linearised RGB [0,1]. Unlike the compositing-formula modes (overlay, hard-light, soft-light) and the HSL-decomposition modes (hue, saturation, color, luminosity), difference is defined by a simple absolute-value operation that has fundamentally different attack properties: its output depends on the sign of Cs − Cb, not on multiplying or clamping.

The security consequence of the absolute-value formula is an inversion attack: when Cb=1 (white in2), |Cs−1|=1−Cs for all Cs in [0,1]. Dark consent text with Cs=0.04 is mapped to 1−0.04=0.96 — near-white. On a white or light background, this near-white text is effectively invisible. The same formula also makes mid-gray in2 a contrast-collapse vector: at Cb=0.5, dark text (Cs=0.04) maps to |0.04−0.5|=0.46 — a mid-tone value with WCAG contrast 2.06:1 against white, far below the 4.5:1 threshold.

Asymmetric attack surface: mode=difference attacks with white in2 (inversion) and mid-gray in2 (contrast collapse). But black in2 (Cb=0) is always |Cs−0|=Cs — perfect identity, no attack. Detection rules must distinguish in2 luminance: flag near-white and near-gray in2, not near-black. Flagging difference+black would be a false positive.

Formula: difference

/* difference */
out = |Cs - Cb|   (per channel, linearised RGB)

/* Extreme-endpoint cases */
out(Cs, Cb=0)  = |Cs-0| = Cs          /* identity — no attack */
out(Cs, Cb=1)  = |Cs-1| = 1-Cs        /* inversion — dark→near-white */
out(Cs, Cb=0.5) = |Cs-0.5|            /* distance from 0.5 */

/* Dark text Cs=0.04 */
difference(0.04, 0)   = 0.04           /* identity */
difference(0.04, 0.5) = 0.46           /* mid-tone — contrast collapse */
difference(0.04, 1)   = 0.96           /* near-white — invisible on white bg */

/* Light text Cs=0.96 */
difference(0.96, 0)   = 0.96           /* identity */
difference(0.96, 0.5) = 0.46           /* same mid-tone as dark text at Cb=0.5 */
difference(0.96, 1)   = 0.04           /* near-black — visible on white bg (not an attack) */

SA-CSS-DIF-001: feBlend mode="difference" + white in2 (Cb=1) — inversion to near-white

A feFlood with flood-color="white" produces Cb=1 for all channels. The difference formula: |Cs−1|=1−Cs for all Cs∈[0,1]. Dark consent text with linearised Cs=0.04 becomes 1−0.04=0.96. WCAG relative luminance of 0.96: contrast ratio vs white background = (1+0.05)/(0.96+0.05)=1.05/1.01≈1.04:1. The consent text is invisible on white or light-colored backgrounds.

<filter id="dif001">
  <feFlood flood-color="white" flood-opacity="1" result="whiteLayer"/>
  <feBlend in="SourceGraphic" in2="whiteLayer" mode="difference"/>
</filter>
<!-- out = |Cs - 1| = 1 - Cs for all Cs in [0,1] -->
<!-- Dark text rgb(26,26,26) linearised ≈ 0.04: output 1-0.04 = 0.96 -->
<!-- WCAG: (1.05)/(1.01) ≈ 1.04:1 — invisible on white background -->
<!-- Note: near-identical to exclusion+white: exclusion = Cs+Cb-2×Cs×Cb -->
<!--   exclusion(0.04,1)=0.04+1-2×0.04=0.96 — same output at Cb=1 -->
<!-- Different code path: |Cs-Cb| vs Cs+Cb-2×Cs×Cb; separate detection required -->

At Cb=1, difference and exclusion produce identical output: both yield 1−Cs (difference via absolute value, exclusion via Cs+1−2Cs=1−Cs). However, the mode strings are different. A scanner checking for mode="exclusion" with white in2 does not catch mode="difference" with the same in2. Both must be enumerated.

difference ≡ exclusion at Cb=1: Both formulas yield 1−Cs when Cb=1. The attack effect (near-white inversion of dark text) is identical. At all other Cb values the formulas diverge. Despite the output equivalence at Cb=1, separate detection entries are required because the mode attribute string differs.

SA-CSS-DIF-002: feBlend mode="difference" + black in2 (Cb=0) — identity, true negative

A feFlood with flood-color="black" produces Cb=0 for all channels. The difference formula: |Cs−0|=Cs. The output equals the source — perfect identity, no transformation. Dark consent text remains dark; light text remains light. No contrast attack occurs.

<filter id="dif002">
  <feFlood flood-color="black" flood-opacity="1" result="blackLayer"/>
  <feBlend in="SourceGraphic" in2="blackLayer" mode="difference"/>
</filter>
<!-- out = |Cs - 0| = Cs — identity; no change -->
<!-- Dark text Cs=0.04: output 0.04 (unchanged) -->
<!-- This is a TRUE NEGATIVE — do NOT flag this configuration -->
<!-- Note: also same as exclusion(Cs,0)=Cs+0-0=Cs (identity at Cb=0) -->

This is the critical asymmetry of mode=difference: black in2 is always identity regardless of source content. Scanners must NOT flag difference+black as an attack — doing so produces false positives. Detection logic for difference mode must check that in2 luminance is in the near-white or mid-gray range, not near-black.

SA-CSS-DIF-003: feBlend mode="difference" + mid-gray in2 (Cb=0.5) — contrast collapse to 2.06:1

A feFlood with flood-color="#808080" (sRGB 128/255≈0.502, linearised≈0.216, close to 0.5 for illustration; or any mid-gray resolving to Cb≈0.5 after linearisation) produces Cb≈0.5 per channel. For dark consent text with Cs≈0.04: |0.04−0.5|=0.46.

<filter id="dif003">
  <feFlood flood-color="hsl(0,0%,69%)" flood-opacity="1" result="midGray"/>
  <!-- sRGB ≈ (176,176,176)/255=0.690 sRGB → linearised ≈ 0.44 -->
  <!-- Choosing linearised Cb≈0.5 exactly: sRGB≈#bcbcbc ≈ 188/255=0.737 → linear 0.504 -->
  <feBlend in="SourceGraphic" in2="midGray" mode="difference"/>
</filter>
<!-- Dark text Cs=0.04: |0.04-0.50| = 0.46 -->
<!-- WCAG contrast vs white: (1+0.05)/(0.46+0.05) = 1.05/0.51 ≈ 2.06:1 — FAIL (need 4.5:1) -->
<!-- Light text Cs=0.96: |0.96-0.50| = 0.46 — same mid-tone result -->
<!-- Both dark and light consent text map to ~0.46 — both fail contrast -->
<!-- feFlood flood-color is not "white" and not "black" — evades endpoint detection -->

The mid-gray attack has an unusual property: both dark text (Cs=0.04) and light text (Cs=0.96) map to approximately the same output value (~0.46), because the difference formula computes distance from Cb=0.5 symmetrically. The attack erases both foreground color conventions simultaneously. WCAG contrast of 0.46-luminance text against white is 2.06:1; against a mid-gray background it may be even lower.

Mid-gray equalises dark and light text: difference+mid-gray maps both Cs=0.04 and Cs=0.96 to approximately 0.46. Dark text on white (original contrast ~19:1) and light text on dark (original contrast ~19:1) both collapse to ~2.06:1 or lower. A mid-gray feFlood simultaneously attacks both foreground conventions — this cannot be circumvented by choosing a contrasting text color.

SA-CSS-DIF-004: JS mousedown flood-color transparent → white — click-time inversion

The initial state uses flood-opacity="0" so the feBlend has no in2 contribution; the feBlend in normal or difference mode with opacity-0 in2 passes the source through with minimal visible effect. On mousedown — when the user activates the consent element — the script sets flood-color to white and flood-opacity to 1. The difference formula with Cb=1 immediately inverts: |Cs−1|=1−Cs, mapping dark text to near-white. The consent text was visible before the click and invisible at the moment of activation.

<svg style="position:absolute;width:0;height:0">
  <defs>
    <filter id="dif004">
      <feFlood id="dif004Flood" flood-color="white" flood-opacity="0" result="fl"/>
      <feBlend id="dif004Blend" in="SourceGraphic" in2="fl" mode="difference"/>
    </filter>
  </defs>
</svg>
<div id="consent" style="filter:url(#dif004)">
  I agree to grant full file-system access to this MCP server.
</div>
<script>
document.getElementById("consent").addEventListener("mousedown", () => {
  document.getElementById("dif004Flood").setAttribute("flood-opacity", "1");
  // difference(Cs, 1) = |Cs-1| = 1-Cs for all Cs
  // Dark text 0.04 → 0.96; near-white; invisible on white background at click time
});
</script>

The click-time inversion pattern is consistent with SA-CSS attacks using lighten, screen, and color-dodge: all produce white-out at click time via different formulas. The difference mode inversion is distinct because it affects all source values — not only dark text is made light, but light text becomes dark (Cs=0.96 → 0.04). Depending on the background color, this may create a double-blindspot: white-background consent text may be dark-on-dark after inversion if the background also contributes.

SA-CSS-DIF-005: feBlend mode="difference" + near-white in2 (Cb=0.94) — near-inversion attack

A feFlood with flood-color="#f0f0f0" (linearised≈0.94) produces Cb≈0.94 per channel. For dark text with Cs=0.04: |0.04−0.94|=0.90. WCAG contrast of output 0.90 vs white background: (1+0.05)/(0.90+0.05)=1.05/0.95≈1.11:1. Invisible. This near-inversion avoids using exact flood-color="white" which might be caught by simple string matching.

<filter id="dif005">
  <feFlood flood-color="#f0f0f0" flood-opacity="1" result="nearWhite"/>
  <!-- sRGB #f0f0f0 = 240/255 ≈ 0.941 sRGB → linearised ≈ 0.871 (gamma 2.2) -->
  <!-- Using approximate linearised Cb=0.94 for clarity -->
  <feBlend in="SourceGraphic" in2="nearWhite" mode="difference"/>
</filter>
<!-- Dark text Cs=0.04: |0.04 - 0.94| = 0.90 -->
<!-- WCAG vs white: (1.05)/(0.95) = 1.11:1 — FAIL -->
<!-- flood-color="#f0f0f0" ≠ "white"; evades exact string detection -->
<!-- threshold: any near-white feFlood (all channels > 0.85) with difference mode -->
<!--   should simulate |Cs - Cb| for dark foreground Cs≈0.04 and check output -->

The threshold for detection should be: if feFlood channel values are all above ~0.85 (linearised), simulate |Cs−Cb| for dark foreground (Cs=0.04) and evaluate WCAG contrast of the output. For Cb>0.85, the output 1−Cs−(1−Cb) is bounded by Cb−Cs which for Cs=0.04 is >0.81; contrast vs white <1.3:1. Near-white in2 with difference mode is always a near-inversion attack for dark text, whether or not the flood-color parses as exact white.

difference vs exclusion — formula comparison at key Cb values

Cb valuedifference: |Cs−Cb|exclusion: Cs+Cb−2CsCbEqual at Cs=0.04?Attack?
0 (black)Cs (identity)Cs (identity)Yes — both 0.04No (true negative)
0.5 (mid-gray)|0.04−0.5|=0.460.04+0.5−0.04=0.46Yes — both 0.46Yes (2.06:1 WCAG fail)
1 (white)|0.04−1|=0.960.04+1−0.08=0.96Yes — both 0.96Yes (1.04:1 WCAG fail)
0.7 (mid-light)|0.04−0.7|=0.660.04+0.7−0.056=0.684No — differ slightlyYes (both reduce contrast)

At Cb=0, 0.5, and 1 the two formulas coincide for Cs=0.04 — the attack outputs are the same despite different math. At intermediate Cb values they diverge slightly but both remain contrast attacks for Cb>0.3. Detection must check both mode="difference" and mode="exclusion" separately; neither covers the other via string matching.

Attack summary

IDModein2 CbFormula result (Cs=0.04)EffectSeverity
SA-CSS-DIF-001difference1 (white)|0.04−1|=0.96Near-white-out; WCAG 1.04:1Critical
SA-CSS-DIF-002difference0 (black)|0.04−0|=0.04Identity — no attack (true negative)—
SA-CSS-DIF-003difference0.5 (mid-gray)|0.04−0.5|=0.46Mid-tone contrast collapse; WCAG 2.06:1High
SA-CSS-DIF-004difference (mousedown)0→10.04→0.96Click-time inversion; near-white-outCritical
SA-CSS-DIF-005difference0.94 (near-white)|0.04−0.94|=0.90Near-inversion; WCAG 1.11:1High

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-DIF-001: feBlend mode="difference" with feFlood flood-color=white (Cb=1) — formula |Cs-1|=1-Cs; dark text Cs=0.04 → 0.96; WCAG (1.05)/(1.01)≈1.04:1; near-white inversion; mathematically equivalent to exclusion+white at Cb=1 but different mode string; separate detection required.
HIGH SA-CSS-DIF-003: feBlend mode="difference" with mid-gray feFlood (0.30 ≤ Cb ≤ 0.70) — formula |Cs-Cb|; dark text Cs=0.04 at Cb=0.5 → 0.46; WCAG ≈2.06:1; both dark and light consent text collapse to the same mid-tone; contrast attack affects all foreground color conventions simultaneously.
CRITICAL SA-CSS-DIF-004: JS interaction handler sets flood-opacity from 0 to 1 (white) on feBlend mode=difference — initial state identity; click-time: |Cs-1|=1-Cs; inversion at moment of consent activation; runtime MutationObserver on feFlood flood-opacity within difference-mode filter chains in consent ancestors required.
HIGH SA-CSS-DIF-005: feBlend mode="difference" with near-white feFlood (Cb≥0.85, e.g. #f0f0f0) — formula |Cs-Cb|≈Cb-Cs; dark text → near-white; WCAG <1.3:1; evades flood-color="white" string match; threshold: any feFlood resolving to linearised channel ≥0.85 in a difference-mode feBlend on a consent ancestor.

Related: feBlend mode=exclusion consent attacks (SA-CSS-EXC)  |  feBlend multiply/screen consent attacks (SA-CSS-MSC)  |  feBlend darken/lighten attacks (SA-CSS-BDL)

← Blog  |  Security Checklist