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.
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 value | difference: |Cs−Cb| | exclusion: Cs+Cb−2CsCb | Equal at Cs=0.04? | Attack? |
|---|---|---|---|---|
| 0 (black) | Cs (identity) | Cs (identity) | Yes — both 0.04 | No (true negative) |
| 0.5 (mid-gray) | |0.04−0.5|=0.46 | 0.04+0.5−0.04=0.46 | Yes — both 0.46 | Yes (2.06:1 WCAG fail) |
| 1 (white) | |0.04−1|=0.96 | 0.04+1−0.08=0.96 | Yes — both 0.96 | Yes (1.04:1 WCAG fail) |
| 0.7 (mid-light) | |0.04−0.7|=0.66 | 0.04+0.7−0.056=0.684 | No — differ slightly | Yes (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
| ID | Mode | in2 Cb | Formula result (Cs=0.04) | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-DIF-001 | difference | 1 (white) | |0.04−1|=0.96 | Near-white-out; WCAG 1.04:1 | Critical |
| SA-CSS-DIF-002 | difference | 0 (black) | |0.04−0|=0.04 | Identity — no attack (true negative) | — |
| SA-CSS-DIF-003 | difference | 0.5 (mid-gray) | |0.04−0.5|=0.46 | Mid-tone contrast collapse; WCAG 2.06:1 | High |
| SA-CSS-DIF-004 | difference (mousedown) | 0→1 | 0.04→0.96 | Click-time inversion; near-white-out | Critical |
| SA-CSS-DIF-005 | difference | 0.94 (near-white) | |0.04−0.94|=0.90 | Near-inversion; WCAG 1.11:1 | High |
Defences
- Add difference to the feBlend mode detection list: The mode enumeration must include: darken, lighten, overlay, soft-light, hard-light, color-dodge, color-burn, exclusion, multiply, screen, hue, saturation, color, luminosity, and now difference. No mode is interchangeable with another for detection purposes.
- Flag near-white in2 (Cb≥0.85) with difference mode: Resolve feFlood flood-color to linearised RGB. If any channel exceeds 0.85 and flood-opacity ≥ 0.95, simulate
|Cs−Cb|for dark foreground Cs=0.04. If output ≥ 0.80, WCAG contrast vs white < 1.4:1; flag as Critical. The formula |Cs−Cb|≈Cb−Cs for Cb>Cs, so near-white in2 always inverts dark text to near-white. - Flag mid-tone in2 (0.30 ≤ Cb ≤ 0.70) with difference mode: Simulate |0.04−Cb| for dark foreground; if output is in [0.26, 0.66], verify WCAG contrast against the expected background color. At Cb=0.5 the output 0.46 fails 4.5:1 against white. Use numeric threshold, not color-name matching.
- Do NOT flag difference+black (Cb=0): The output is always identity. Flagging this is a false positive. The detection must require Cb > 0 (or Cb > some small epsilon like 0.05).
- Runtime MutationObserver: Monitor changes to
flood-colorandflood-opacityon feFlood elements that feed into feBlend nodes with mode="difference" in the filter graph of any consent ancestor element. A transition from opacity-0 to opacity-1 with near-white flood-color is the click-time inversion attack pattern (SA-CSS-DIF-004). - Threshold-based numeric analysis: String matching on flood-color="white" misses #f0f0f0, hsl(0,0%,94%), rgb(240,240,240), and other near-white representations. Parse flood-color to linearised per-channel values and check numerically against the ≥0.85 threshold.
SkillAudit findings for this attack surface
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.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.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.