← Blog

MCP server SVG feComposite xor consent security

Four SA-CSS-XOR consent attack patterns using feComposite operator="xor" — the Porter-Duff exclusive-or compositing formula out_alpha = αA×(1−αB) + αB×(1−αA) exploited to reduce opaque consent text to near-zero alpha, cancel elements via self-XOR, and trigger erasure precisely at click time via JS flood-opacity override.

By SkillAudit  |  24 Oct 2026

The feComposite SVG filter primitive composites two images using a Porter-Duff formula selected by the operator attribute. The operator=xor formula implements exclusive-or compositing: out_alpha = αA×(1−αB) + αB×(1−αA) (SVG §15.13). Geometrically, the XOR operator keeps only the portions of in1 and in2 that do not overlap — pixels covered by both are cancelled, and only the non-overlapping regions survive.

The XOR operator is the least commonly audited feComposite operator precisely because it has no intuitive visual analogue in standard graphic design workflows. Security reviewers familiar with operator=in (intersection-masking), operator=out (difference-masking), and operator=atop (A-in-B) often skip the XOR case — its alpha-cancellation behavior at high in2 opacity creates a near-transparent result that looks like CSS opacity but is invisible to getComputedStyle().opacity.

XOR with near-opaque in2: When αB approaches 1 (e.g., flood-opacity=0.999), the formula for fully opaque in1 pixels (αA=1) gives: out_alpha = 1×(1−0.999) + 0.999×(1−1) = 0.001 + 0 = 0.001. The consent element is rendered at 0.1% alpha. This passes any detection that checks for exact-zero alpha (outputAlpha === 0), but is functionally invisible against any background.

SA-CSS-XOR-001: feComposite operator="xor" with near-opaque in2 (near-zero alpha output)

A feFlood with flood-opacity="0.999" produces a near-opaque image (αB≈1). When used as in2 in an XOR composite with in1=SourceGraphic (opaque consent text, αA=1), the formula gives: out_alpha = 1×(1−0.999) + 0.999×(1−1) = 0.001. Every opaque pixel of the consent element is reduced to 0.1% alpha.

<filter id="xor1">
  <feFlood flood-color="white" flood-opacity="0.999" result="nearOpaque"/>
  <feComposite in="SourceGraphic" in2="nearOpaque" operator="xor"/>
</filter>
<!-- αA=1 (opaque text), αB=0.999: out_alpha=0.001; element at 0.1% opacity -->
<!-- getComputedStyle().opacity still reports "1"; no CSS opacity property set -->

The rendered alpha of 0.001 is functionally invisible — contrast of any color at 0.1% alpha against any background is below 1.01:1 — but passes detection that checks outputAlpha === 0 or alpha < 0.001 with a strict equality threshold. The correct detection threshold is maxOutputAlpha(C_in=1) < 0.05: evaluate the XOR formula for αA=1 against the attacker-controlled αB; if the resulting out_alpha is below 0.05, flag SA-CSS-XOR-001.

The flood-color value is irrelevant for this attack — only flood-opacity matters, because the XOR cancellation acts entirely in the alpha channel. Any flood color (white, black, transparent background, brand color) with flood-opacity=0.999 produces the same near-zero output alpha.

SA-CSS-XOR-002: feComposite operator="xor" with 50% in2 opacity (half-alpha rendering)

A feFlood with flood-opacity="0.5" gives αB=0.5. For opaque consent text (αA=1): out_alpha = 1×0.5 + 0.5×0 = 0.5. The entire consent element renders at 50% alpha. Against a white page background, the dark consent text composites to a medium gray — its effective contrast against the white background drops to half the original value.

<filter id="xor2">
  <feFlood flood-color="white" flood-opacity="0.5" result="halfOpaque"/>
  <feComposite in="SourceGraphic" in2="halfOpaque" operator="xor"/>
</filter>
<!-- αA=1, αB=0.5: out_alpha=0.5; element at 50% alpha -->
<!-- dark text #374151 (rgb 55,65,81) at 50% alpha on white → effective color rgb(155,160,168) -->
<!-- WCAG contrast of rgb(155,160,168) vs white ≈ 2.2:1; below 4.5:1 AA threshold -->

WCAG at 50% alpha: Dark gray text at 50% alpha composites against white as a lighter gray. For consent text using #374151 (a common UI dark-gray): the premultiplied composite against white yields approximately rgb(155,160,168) — relative luminance ≈ 0.37; WCAG contrast vs white ≈ 2.2:1. This is below the 4.5:1 WCAG AA threshold for normal text and below 3:1 for large text. The attack degrades readability without triggering zero-alpha detection.

SA-CSS-XOR-003: feComposite operator="xor" with in=in2=SourceGraphic (self-XOR full erasure)

When both in and in2 reference the same image source — in="SourceGraphic" in2="SourceGraphic" — the XOR formula computes out_alpha = αA×(1−αA) + αA×(1−αA) = 2×αA×(1−αA). For any fully opaque pixel (αA=1): out_alpha = 2×1×0 = 0. For any fully transparent pixel (αA=0): out_alpha = 0. Only partially transparent pixels (0<αA<1, anti-aliased edges) survive, at out_alpha = 2×αA×(1−αA) — maximized at αA=0.5 with out_alpha=0.5, and zero at both endpoints.

<filter id="xor3">
  <feComposite in="SourceGraphic" in2="SourceGraphic" operator="xor"/>
</filter>
<!-- αA=αB for all pixels; out_alpha=2×αA×(1-αA) -->
<!-- αA=1 (opaque text pixels): out_alpha=0 → fully transparent -->
<!-- αA=0 (transparent pixels): out_alpha=0 → remains transparent -->
<!-- αA=0.5 (anti-aliased edge): out_alpha=0.5; visible as faint shadow-outline only -->

The result is a full erasure of all opaque content. Only anti-aliased glyph edge pixels leave a faint ghosting trace (peak alpha 0.5 at αA=0.5 edges). The consent element is effectively invisible — the text body disappears and only a barely perceptible outline artifact remains. Detection must recognize the self-reference pattern: any feComposite where in and in2 resolve to the same image (including via result references that cycle back to SourceGraphic) with operator=xor is a guaranteed full erasure.

SA-CSS-XOR-004: JS mousedown sets feFlood flood-opacity from 0 to 0.999 (click-time erasure)

When flood-opacity is 0 (αB=0), the XOR formula gives: out_alpha = αA×(1−0) + 0×(1−αA) = αA. The output alpha equals the input alpha — this is the identity operation for the alpha channel. The consent element renders normally at flood-opacity=0. On mousedown, the script sets flood-opacity to 0.999, activating SA-CSS-XOR-001: out_alpha drops to 0.001 for all opaque pixels.

consentElement.addEventListener("mousedown", () => {
  const flood = document.getElementById("xorFilter")
                         .querySelector("feFlood");
  flood.setAttribute("flood-opacity", "0.999"); // αB → 0.999; XOR cancels opaque pixels
  // out_alpha = αA×0.001 = 0.001 for αA=1; element near-invisible
  // state reset after install flow if needed
  setTimeout(() => flood.setAttribute("flood-opacity", "0"), 500);
});

The initial state (flood-opacity=0) is unambiguously legitimate — a feFlood with zero opacity is a no-op in most compositing formulas. Static analysis of the initial filter state sees no attack. The XOR identity property at αB=0 is specific to the XOR operator: for in and out operators, αB=0 also produces identity-like behavior. This cross-operator commonality may mislead static tools that check "flood-opacity=0 is safe" as a blanket rule without considering operator-specific behavior at other αB values.

Attack summary

IDin2 stateout_alpha (αA=1)EffectSeverity
SA-CSS-XOR-001feFlood flood-opacity=0.9990.001Near-full transparency; passes exact-zero detectionCritical
SA-CSS-XOR-002feFlood flood-opacity=0.50.550% alpha; WCAG contrast halvedHigh
SA-CSS-XOR-003in=in2=SourceGraphic (self-XOR)0Full erasure; opaque pixels cancelCritical
SA-CSS-XOR-004flood-opacity 0→0.999 mousedown1→0.001Identity at load; erasure at clickCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-XOR-001: feComposite operator="xor" with near-opaque in2 (feFlood flood-opacity≥0.95) — XOR alpha formula out_alpha=αA×(1-αB)+αB×(1-αA); αA=1 (opaque consent text): out_alpha=1-αB; at αB=0.999: out_alpha=0.001 (0.1% opacity); functionally invisible; passes alpha===0 exact check; threshold must be out_alpha(αA=1)<0.05.
HIGH SA-CSS-XOR-002: feComposite operator="xor" with feFlood flood-opacity=0.5 — αA=1: out_alpha=0.5; 50% alpha rendering; dark text at 50% alpha on white background produces effective ~2.2:1 WCAG contrast for typical UI grays; below 4.5:1 AA threshold; threshold: out_alpha(αA=1)<0.8 → flag.
CRITICAL SA-CSS-XOR-003: feComposite operator="xor" with in="SourceGraphic" in2="SourceGraphic" (self-XOR) — out_alpha=2×αA×(1-αA); αA=1 → out_alpha=0; αA=0 → out_alpha=0; all opaque pixels fully transparent; anti-aliased edges survive at reduced alpha; element body invisible; detect via in/in2 same-source resolution.
CRITICAL SA-CSS-XOR-004: JS mousedown sets feFlood flood-opacity from 0 (identity αB=0) to 0.999 (SA-CSS-XOR-001) — initial state correct; static analysis sees identity; click-time: αB=0.999; out_alpha=0.001; MutationObserver on feFlood flood-opacity attribute in operator=xor composite within consent ancestor filter required.

Related: feComposite operator=atop attacks  |  feComposite operator=over with white in2  |  feComposite operator overview

← Blog  |  Security Checklist