MCP server SVG feBlend mode=exclusion consent security
Four SA-CSS-EXC consent attack patterns using feBlend mode="exclusion" — the symmetric subtraction formula that inverts dark consent text to near-white via white in2, collapses all output to a constant mid-gray via a mid-linearRGB in2, leaves source unchanged via black in2 (an identity, not an attack), and activates contrast collapse at click time by mutating only the feFlood flood-color rather than the blend mode.
The feBlend SVG filter primitive's mode="exclusion" formula is defined in W3C Compositing and Blending Level 1 as out = Cs + Cb − 2 × Cs × Cb, where Cs is the source (in1, the consent element rendered as SourceGraphic) and Cb is the backdrop (in2, attacker-controlled via feFlood). All values are normalized to [0,1] in linear RGB. The formula is symmetric — swapping in1 and in2 produces identical output — and its behaviour depends critically on the value of Cb.
Three special cases dominate the attack surface. At Cb=0: out = Cs + 0 − 0 = Cs — identity, no modification. At Cb=0.5: out = Cs + 0.5 − Cs = 0.5 — constant output, all pixels become mid-gray regardless of source. At Cb=1: out = Cs + 1 − 2Cs = 1 − Cs — inversion of the source channel. These three analytical fixed points define which in2 values are attacks (Cb=1 and Cb=0.5) versus inert (Cb=0).
Exclusion has two distinct attack vectors: Unlike lighten (white in2 only) or darken (black in2 only), exclusion attacks require either a near-white in2 (Cb≈1, inversion) or a mid-linearRGB in2 (Cb≈0.5, constant output). Black in2 is NOT an attack for exclusion — it is identity. Detection logic must be exclusion-specific; rules derived from other blend modes will either miss exclusion attacks or generate false positives on the benign black in2 case.
The exclusion formula: four key analytical properties
Understanding the formula out = Cs + Cb − 2CsCb analytically before examining individual attacks clarifies the full detection space.
Exclusion formula (W3C compositing-1): out = Cs + Cb - 2 × Cs × Cb Property 1 — Symmetry: out(Cs, Cb) = Cs + Cb - 2CsCb = Cb + Cs - 2CbCs = out(Cb, Cs) Swapping in1/in2 produces identical output. Property 2 — Identity at Cb=0 (black in2): out = Cs + 0 - 2×Cs×0 = Cs Black in2 → output equals source; NO attack. Property 3 — Constant output at Cb=0.5 (mid-gray in2): out = Cs + 0.5 - 2×Cs×0.5 = Cs + 0.5 - Cs = 0.5 ALL output pixels → 0.5 regardless of Cs; constant-output attack. Property 4 — Inversion at Cb=1 (white in2): out = Cs + 1 - 2×Cs×1 = 1 - Cs Output is the complement of source; dark text (Cs≈0.04) → 0.96 (near-white). Sensitivity near Cb=0.5: d(out)/d(Cb) at fixed Cs = 1 - 2Cs For dark text Cs=0.04: d(out)/d(Cb) = 0.92; output is highly sensitive to Cb near 0.5.
sRGB vs linearRGB for the Cb=0.5 constant-output condition: Browsers process feBlend in linearRGB color space. The flood-color #808080 has sRGB value 0.502 but linearRGB value 0.216 — far from 0.5. For exact Cb=0.5 linearRGB (the constant-output condition), the required flood-color is approximately #bcbcbc (sRGB 0.737 → linearRGB ≈ 0.502). Using #808080 gives Cb=0.216 linearRGB: out = 0.568Cs + 0.216; for dark text Cs=0.04: out ≈ 0.239 (contrast vs white ≈3.6:1 — borderline failure, not invisibility). The exact constant-output attack requires knowing the sRGB→linearRGB transfer function.
SA-CSS-EXC-001: feBlend mode="exclusion" with white in2 (dark text inverted to near-white)
A feFlood with flood-color="white" sets Cb=1 per channel in linearRGB. Applying the exclusion formula: out = Cs + 1 − 2Cs = 1 − Cs. Dark consent text with Cs≈0.04 linearRGB (approximately #333333 in sRGB) produces out = 1 − 0.04 = 0.96 — a near-white value. Against a white or light background, this renders as invisible. Lighter dark text (e.g., #555555, Cs≈0.09) yields out = 0.91, still near-white. Any dark foreground with Cs<0.1 will produce out>0.9 — effectively invisible on light backgrounds.
<filter id="exclusion1"> <feFlood flood-color="white" flood-opacity="1" result="whiteBackdrop"/> <feBlend in="SourceGraphic" in2="whiteBackdrop" mode="exclusion"/> </filter> <!-- With Cb=1 (white): out = 1 - Cs (inversion formula) --> <!-- Dark text Cs≈0.04 (#333): out = 1-0.04 = 0.96 (near-white) --> <!-- WCAG: relative luminance 0.96 vs white bg 1.0: (1.05)/(0.96+0.05) ≈ 1.04:1 --> <!-- getComputedStyle().color still reports original #333333; filter invisible to DOM --> <!-- IMPORTANT: output is NOT constant — it varies with Cs (inversion); --> <!-- constant-output detectors that check max-min output range will miss this attack -->
A critical detection consideration: this is an inversion attack, not a constant-output attack. The output varies with Cs — different input text colors produce different near-white outputs. A detector that identifies constant-output feBlend configurations (output range max−min < 0.1 across all Cs) will correctly flag EXC-002 but will miss EXC-001. Detection for EXC-001 must evaluate: for the expected foreground color Cs of the consent element, does the exclusion formula with the observed Cb produce out>0.9 (near-white) while the page background is light (luminance>0.8)? If so, flag inversion white-out.
SA-CSS-EXC-002: feBlend mode="exclusion" with mid-gray in2 (Cb=0.5 linearRGB, constant output)
When the feFlood sets Cb=0.5 in linearRGB, the exclusion formula reduces to a constant: out = Cs + 0.5 − 2Cs×0.5 = Cs + 0.5 − Cs = 0.5. Every output pixel becomes exactly mid-gray (0.5 linearRGB), regardless of the source. Dark consent text (#333), mid-gray text (#888), and light text (#ccc) all render identically as a flat mid-gray field. This is a true constant-output attack — the output carries zero information from the source.
<filter id="exclusion2"> <!-- #bcbcbc: sRGB=0.737 → linearRGB≈0.502 ≈ 0.5; exact constant-output condition --> <feFlood flood-color="#bcbcbc" flood-opacity="1" result="midGrayBackdrop"/> <feBlend in="SourceGraphic" in2="midGrayBackdrop" mode="exclusion"/> </filter> <!-- Cb≈0.5 linearRGB: out = Cs + 0.5 - Cs = 0.5 for ALL Cs values --> <!-- Dark text Cs=0.04 → out=0.5; mid-gray text Cs=0.5 → out=0.5; light text → out=0.5 --> <!-- WCAG on white background (luminance=1): (1+0.05)/(0.5+0.05) = 1.05/0.55 ≈ 1.91:1 --> <!-- WCAG AA requires 4.5:1; WCAG AAA requires 7:1; both fail --> <!-- On mid-gray background (luminance≈0.5): (0.55)/(0.55) = 1:1 — total failure --> <!-- #808080 ≠ Cb=0.5 linearRGB; #808080 gives Cb=0.216 lRGB → not constant-output -->
On a white background (linearRGB luminance=1.0), the WCAG 2.1 relative luminance contrast between mid-gray output (0.5) and white is (1.0+0.05)/(0.5+0.05) = 1.05/0.55 ≈ 1.9:1 — far below the WCAG AA threshold of 4.5:1. On a mid-gray background matching the output, contrast is 1:1. On a dark background (#1e1e2e, common in AI agent installer themes), the contrast between mid-gray and dark is somewhat higher but the consent text still carries no semantic information since all characters render identically.
The #808080 misconception: A naive attacker might use flood-color="#808080" expecting mid-gray. But #808080 has sRGB value ≈0.502, which converts to linearRGB via the sRGB EOTF as approximately 0.216. This is NOT 0.5 linearRGB. The formula gives out = 0.568Cs + 0.216 — for dark text (Cs=0.04): out≈0.239. Contrast vs white ≈3.6:1 — a borderline failure, more detectable and less absolute than the true constant-output condition. The actual constant-output flood color for linearRGB=0.5 is #bcbcbc (sRGB≈0.737). Security tools must perform the sRGB→linearRGB conversion before evaluating the exclusion formula.
SA-CSS-EXC-003: feBlend mode="exclusion" with black in2 (identity — not an attack)
A feFlood with flood-color="black" sets Cb=0 per channel. The exclusion formula gives: out = Cs + 0 − 2Cs×0 = Cs. The output equals the source — exclusion with black in2 is a pure identity transform. The consent element renders with its original colors and contrast completely intact. This is not an attack.
<filter id="exclusion3"> <feFlood flood-color="black" flood-opacity="1" result="blackBackdrop"/> <feBlend in="SourceGraphic" in2="blackBackdrop" mode="exclusion"/> </filter> <!-- Cb=0 (black): out = Cs + 0 - 2×Cs×0 = Cs (identity) --> <!-- Dark text Cs=0.04 → out=0.04; original color preserved; NO attack --> <!-- This contrasts with mode=darken (black in2 → forces all to black = attack) --> <!-- and mode=overlay/hard-light (black in2 → attacks light text) --> <!-- Exclusion-specific detection logic required; generic "dark in2 = suspicious" false-positives here -->
Why this case matters for detection quality: Detection rules that flag any feBlend with a dark or black in2 flood will correctly identify darken+black and overlay+black attacks, but will generate false positives for exclusion+black — which is a benign identity. A security tool that reports SA-CSS-EXC-003 as a finding has a logic error. The correct rule is: for mode=exclusion, flag in2 feFlood values where Cb≈0.5 linearRGB (constant output) OR Cb≈1 linearRGB (inversion); do NOT flag Cb≈0 (black in2) for exclusion mode.
SA-CSS-EXC-004: JS mousedown mutates feFlood flood-color from Cb=0 to Cb=0.5 (click-time constant-output)
The initial filter state has the feFlood at flood-color="black" (Cb=0). By the identity property (EXC-003), the consent element renders correctly — the exclusion filter is inert. The feBlend mode attribute remains "exclusion" throughout; the mode never changes. On mousedown, the script mutates only the feFlood flood-color attribute from black to "#bcbcbc" (Cb≈0.5 linearRGB). The constant-output condition activates: all pixels collapse to 0.5 mid-gray. Consent text becomes unintelligible.
consentElement.addEventListener("mousedown", () => {
const flood = document.getElementById("exclusionFilter")
.querySelector("feFlood");
// Switch flood-color from identity (Cb=0) to constant-output (Cb≈0.5 linearRGB)
flood.setAttribute("flood-color", "#bcbcbc");
// feBlend mode="exclusion" is unchanged throughout — mode-only observer misses this
// Before: out=Cs (identity); After: out=0.5 (constant mid-gray) for all Cs
// User perceives correct consent text before click; flat gray field during click
});
This attack is particularly stealthy because the mutation target is the flood-color attribute on a feFlood child element — not the mode attribute on the feBlend element itself. A MutationObserver configured to watch feBlend attribute changes (the typical implementation for detecting SA-CSS-BDL-004 and SA-CSS-BCB-004 click-time attacks) will not fire. The observer must specifically monitor feFlood elements that supply in2 to feBlend mode="exclusion" primitives within consent ancestor filters. Both flood-color and flood-opacity attributes on those feFlood nodes require observation, since flood-opacity<1 also shifts the effective Cb.
// Correct MutationObserver implementation for exclusion flood-color attacks:
const filter = consentElement.closest("[filter]");
const filterId = filter?.getAttribute("filter")?.match(/url\(#([^)]+)\)/)?.[1];
const filterEl = document.getElementById(filterId);
if (!filterEl) return;
filterEl.querySelectorAll("feBlend[mode='exclusion']").forEach(blend => {
const in2Id = blend.getAttribute("in2");
const in2El = filterEl.querySelector(`[result='${in2Id}']`);
if (in2El?.tagName === "feFlood") {
// Watch flood-color AND flood-opacity on this feFlood
new MutationObserver(mutations => {
for (const m of mutations) {
if (m.attributeName === "flood-color" || m.attributeName === "flood-opacity") {
// Re-evaluate Cb in linearRGB; check for Cb≈0.5 or Cb≈1 attack condition
auditExclusionFilter(blend, in2El);
}
}
}).observe(in2El, { attributes: true });
}
});
Attack summary
| ID | in2 Cb (linearRGB) | Formula result | Output for dark text (Cs=0.04) | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-EXC-001 | White (Cb=1) | 1−Cs (inversion) | 0.96 (near-white) | Color inversion → white-out on light background | Critical |
| SA-CSS-EXC-002 | Mid-gray (Cb≈0.5 lRGB, flood #bcbcbc) |
0.5 (constant) | 0.5 (constant mid-gray) | Constant mid-gray output; contrast vs white ≈1.9:1 | Critical |
| SA-CSS-EXC-003 | Black (Cb=0) | Cs (identity) | 0.04 (identity, original color) | No attack; identity formula; black in2 is benign for exclusion | Medium (note) |
| SA-CSS-EXC-004 | Cb: 0→0.5 lRGB at mousedown | Identity→constant (0.5) | 0.04→0.5 at click time | Click-time constant-output; mode unchanged; mode-only observer blind | Critical |
Defences
- Exclusion-specific formula evaluation: For any
feBlend mode="exclusion"in a consent ancestor filter, identify the in2 source. If in2 is a feFlood, parse the flood-color to linearRGB channel values Cb. Evaluateout = Cs + Cb − 2CsCbfor the expected consent foreground (Cs≈0.05 for dark text). If out>0.9 (inversion white-out, EXC-001): flag. If output range across all Cs values [0,1] is less than 0.1 (constant-output, EXC-002): flag. - sRGB-to-linearRGB conversion is mandatory: The constant-output condition occurs at Cb=0.5 linearRGB, which corresponds to flood-color
#bcbcbc— not#808080. Do not evaluate feFlood colors in sRGB space; apply the IEC 61966-2-1 sRGB EOTF to each channel before substituting into the exclusion formula. - Do not flag black in2 for exclusion: Cb=0 produces identity for exclusion. A rule that flags feBlend mode=exclusion with dark in2 will generate false positives. Exclusion detection logic must be isolated from darken/overlay/hard-light logic that does flag black in2.
- Inversion detection requires context: EXC-001 produces near-white output (out≈0.96) for dark text. This is not constant-output — the output value varies with Cs. Detection must evaluate the specific expected foreground color against the page background luminance, not just check for constant output. If out>0.9 AND page background luminance>0.8: flag as white-out inversion attack.
- MutationObserver must target feFlood flood-color, not feBlend mode: The EXC-004 attack mutates the feFlood flood-color attribute while leaving the feBlend mode unchanged. MutationObservers watching only the feBlend node's attributes will not detect this transition. The observer must be placed on feFlood nodes that supply in2 to any feBlend mode=exclusion primitive within a consent ancestor filter. Watch both
flood-colorandflood-opacityattributes on those feFlood nodes. - Sensitivity near Cb=0.5: The output sensitivity d(out)/d(Cb) at Cs=0.04 is 0.92 — small changes in Cb near 0.5 produce large changes in output. ML-based or heuristic detectors trained on exact-value thresholds may be unstable in this region. Use absolute formula evaluation rather than approximate gradient-based heuristics.
SkillAudit findings for this attack surface
feBlend mode="exclusion" with feFlood flood-color near-white (Cb≈1 linearRGB) in consent ancestor filter — exclusion inversion formula out=1−Cs; dark text Cs≈0.04 → out=0.96 (near-white); WCAG vs white ≈1.04:1; NOT constant-output — constant-output detectors miss this; detection requires per-Cs evaluation of output vs page background luminance.feBlend mode="exclusion" with feFlood flood-color #bcbcbc (Cb≈0.5 linearRGB) in consent ancestor filter — exclusion constant-output formula out=0.5 for all Cs; all consent text renders as flat mid-gray; WCAG on white ≈1.9:1 (AA requires 4.5:1); sRGB→linearRGB conversion required for correct detection; #808080 is NOT Cb=0.5 lRGB.feBlend mode="exclusion" with feFlood flood-color black (Cb=0) — identity formula; no attack; detection tools must not flag this as a finding; exclusion+black is distinct from darken+black; mode-specific logic required to avoid false positives in consent filter analysis.#bcbcbc (Cb≈0.5 lRGB, constant output=0.5) in feBlend mode=exclusion consent filter — mode attribute unchanging throughout; mode-only MutationObserver blind; correct defence: observe flood-color and flood-opacity attributes on feFlood in2 nodes of consent ancestor exclusion filters.