MCP server SVG feComposite atop consent security
Three SA-CSS-ATOP consent attack patterns using feComposite operator="atop" — the Porter-Duff A-atop-B compositing formula out = in1 × αB + in2 × (1 − αA) exploited to erase consent elements, bleach anti-aliased glyph edges, and apply click-time erasure via flood-opacity JS override.
The feComposite SVG filter primitive composites two images using a Porter-Duff formula selected by the operator attribute. The operator=atop formula is out = in1 × αB + in2 × (1 − αA), where αA is the alpha of in1 and αB is the alpha of in2 (SVG §15.13). This formula is geometrically the "A painted inside B" operation: in1 pixels survive only where in2 is opaque (αB), and in2 pixels fill the transparent regions of in1 (1 − αA).
The atop operator differs from the more commonly analyzed operator=in in a critical way: in uses out = in1 × αB (in1 survives only where in2 is opaque); atop additionally adds in2 × (1 − αA) (in2 fills where in1 is transparent). This additional term creates anti-aliased edge bleach attacks that have no equivalent in the in operator family.
Atop with transparent in2: When in2 has zero alpha everywhere (e.g., a feFlood with flood-opacity=0), both terms of the formula collapse: out = in1 × 0 + 0 × (1 − αA) = 0. Every output pixel is zero — both zero color and zero alpha. The element is fully transparent. This is the same erasure result as operator=in with a transparent mask, but achieved via the atop formula from a different code path.
SA-CSS-ATOP-001: feComposite operator="atop" with transparent in2 (full erasure)
A feFlood with flood-opacity="0" produces a fully transparent image. When used as in2 in an atop composite with in1=SourceGraphic, the formula gives: out = SourceGraphic × 0 + (flood-color) × (1 − αA). But the flood-color pixels have zero alpha (flood-opacity=0), so their RGB contribution is zero regardless of the (1 − αA) term. The output is fully transparent on all pixels.
<filter id="atop1"> <feFlood flood-color="white" flood-opacity="0" result="transparentFlood"/> <feComposite in="SourceGraphic" in2="transparentFlood" operator="atop"/> </filter> <!-- αB=0: out = SourceGraphic×0 + transparent×(1-αA) = 0; full erasure -->
The distinction between this attack and the operator=in equivalent (SA-CSS-FCO-003: in with transparent mask) is in the code path, not the output. Detection must check operator=atop independently from operator=in; the erasure condition for atop is αB=0 on in2, identical to the in operator but with a different formula to evaluate.
SA-CSS-ATOP-002: operator="atop" with white opaque in2 (anti-aliased edge bleach)
When in2 is a white opaque feFlood (flood-color="white", flood-opacity=1, αB=1), the atop formula becomes: out = in1 × 1 + white × (1 − αA) = in1 + white × (1 − αA).
For fully opaque in1 pixels (αA=1): out = in1 + white × 0 = in1 — identity, the pixel is unchanged. These are the interior fully-opaque pixels of consent text.
For partially transparent in1 pixels (αA < 1): out = in1 + white × (1 − αA) — the in2 white bleaches toward white in proportion to (1 − αA). Anti-aliased glyph edges have αA values ranging continuously from 0 to 1. A glyph edge pixel with αA=0.3 receives out = 0.3 × in1 + 0.7 × white: the dark foreground color is mixed with 70% white.
<filter id="atop2"> <feFlood flood-color="white" flood-opacity="1" result="whiteFlood"/> <feComposite in="SourceGraphic" in2="whiteFlood" operator="atop"/> </filter> <!-- αB=1: out = in1 + white×(1-αA); opaque pixels unchanged; anti-aliased edges bleached to white -->
WCAG at fully-opaque pixels is still passing: The interior opaque pixels of consent text are unchanged (αA=1 → identity). A WCAG contrast measurement that samples only fully-opaque foreground pixels will report the original contrast ratio. The attack destroys readability at the glyph edges — the anti-aliased transition zone — without affecting the interior. For typical sans-serif consent text at 14px, approximately 30–50% of visible pixels are anti-aliased edge pixels; bleaching these toward white causes the text to appear faded and low-contrast even when interior pixel contrast is nominally passing.
SA-CSS-ATOP-003: JS mousedown on feFlood flood-opacity 1 → 0 (click-time full erasure)
The initial state uses in2=feFlood with flood-opacity=1. With αB=1, the atop operator acts approximately as identity for opaque pixels (with edge bleach). The element renders with slightly degraded anti-aliasing. On mousedown, the script sets flood-opacity to 0: αB drops to 0, and SA-CSS-ATOP-001 takes effect — full erasure of all output pixels. The element disappears at click time.
consentElement.addEventListener("mousedown", () => {
const flood = document.getElementById("consentFilter")
.querySelector("feFlood");
flood.setAttribute("flood-opacity", "0"); // αB → 0; atop formula collapses to 0
// full output erasure; element transparent; click proceeds on invisible element
});
The initial state (flood-opacity=1) is not a suspicious initial value — feFlood with flood-opacity=1 is a legitimate compositing input. Static analysis must check for the presence of a mousedown listener on the consent DOM subtree that targets feFlood flood-opacity within a consent ancestor filter. MutationObserver on the flood-opacity attribute of feFlood elements in such filters is the runtime defence.
Attack summary
| ID | in2 state | αB | Effect | Severity |
|---|---|---|---|---|
| SA-CSS-ATOP-001 | feFlood flood-opacity=0 | 0 | Full output erasure → transparent | Critical |
| SA-CSS-ATOP-002 | feFlood white flood-opacity=1 | 1 | Anti-aliased edge bleach → low contrast | High |
| SA-CSS-ATOP-003 | flood-opacity 1→0 mousedown | 1→0 | Click-time full erasure | Critical |
Defences
- Detect operator=atop with αB=0 in2: For each feComposite with operator=atop in a consent ancestor filter, trace the in2 image to its source primitive. If in2 resolves to a feFlood with flood-opacity ≤ 0.01, or to any zero-alpha source, flag SA-CSS-ATOP-001.
- Detect edge-bleach via αB=1 white in2: For operator=atop with in2 resolving to feFlood flood-color near-white and flood-opacity near-1, flag SA-CSS-ATOP-002. The bleach effect is proportional to (1 − αA) — evaluate on representative anti-aliased edge pixels (αA=0.2, 0.4) to measure WCAG impact.
- MutationObserver on feFlood flood-opacity in consent ancestor filters: Any flood-opacity attribute change on a feFlood used as in2 in an operator=atop composite within a consent ancestor filter triggers SA-CSS-ATOP-003. Monitor directly.
- Compare operator=atop vs operator=in: Both require separate detection paths. The atop formula adds the (in2 × (1-αA)) bleach term not present in in. Don't share detection code between operators without accounting for this difference.
SkillAudit findings for this attack surface
feComposite operator="atop" with transparent in2 (feFlood flood-opacity=0) — atop formula out=in1×αB+in2×(1-αA); αB=0 collapses first term to 0; αB=0 also makes flood pixels transparent; second term contributes 0; full output erasure; same effect as operator=in with transparent mask via different formula path.feComposite operator="atop" with white opaque in2 (feFlood flood-opacity=1) — αB=1; out=in1+white×(1-αA); opaque pixels identity; anti-aliased glyph edges (partial αA) bleached toward white by (1-αA) factor; consent text readability destroyed at edges; WCAG interior-pixel check may still pass; edge-pixel sampling required.feFlood flood-opacity from 1 to 0 within operator=atop composite — initial state: in2 opaque (αB=1); SA-CSS-ATOP-002 edge bleach effect; click-time: αB=0; SA-CSS-ATOP-001 full erasure; MutationObserver on feFlood flood-opacity in consent ancestor filters required.