← Blog

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.

By SkillAudit  |  24 Oct 2026

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

IDin2 Cb (linearRGB)Formula resultOutput for dark text (Cs=0.04)EffectSeverity
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

SkillAudit findings for this attack surface

CRITICAL SA-CSS-EXC-001: 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.
CRITICAL SA-CSS-EXC-002: 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.
MEDIUM SA-CSS-EXC-003: 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.
CRITICAL SA-CSS-EXC-004: JS mousedown mutates feFlood flood-color from black (Cb=0, identity) to #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.

Related: feBlend multiply/screen consent attacks  |  feBlend mode=overlay consent attacks  |  feBlend darken/lighten consent attacks  |  feBlend mode=luminosity consent attacks

← Blog  |  Security Checklist