MCP server SVG feComposite operator consent security
Four SA-CSS-FCO consent attack patterns using the feComposite operator attribute — in, out, xor, and atop compositing with extreme in2 masks to selectively erase consent text — with JS mousedown operator injection for click-time attacks.
The feComposite SVG filter primitive composites two images using an alpha compositing formula. The operator attribute selects among over, in, out, atop, xor, and arithmetic. Unlike feBlend, which operates on color channels using blending modes, feComposite operates primarily on the alpha channel using Porter-Duff compositing. This makes it a precise mask erasure tool: by controlling the in2 alpha map, an attacker can deterministically control which pixels of the consent text survive.
Porter-Duff compositing property: For operator="in", the SVG spec defines out_alpha = in1_alpha × in2_alpha. If in2_alpha = 0 at a pixel position, the output alpha is exactly 0 at that position, regardless of the source alpha. A mask with in2_alpha = 0 everywhere except a 1×1 pixel region erases 99.99% of a typical consent panel — all consent text is transparent except the single unmasked pixel.
SA-CSS-FCO-001: feComposite operator="in" with tiny point mask
The in operator formula is out_alpha = in1_alpha × in2_alpha and out_color = in1_color × in2_alpha. When in2 is a mask image with alpha=1 only at a single 1×1 pixel and alpha=0 everywhere else, the multiplication makes every output pixel transparent except the unmasked point. An SVG <feImage> referencing a data URI with a 1-pixel opaque dot serves as the in2 mask. The consent panel element remains in the DOM, textContent is intact, and getComputedStyle.color returns the original foreground color — only the rendered output is transparent.
<filter id="fco1">
<feImage href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'
width='1' height='1'%3E%3Crect width='1' height='1' fill='white'/%3E%3C/svg%3E"
result="tiny_mask" x="0" y="0" width="1" height="1"/>
<feComposite in="SourceGraphic" in2="tiny_mask" operator="in"/>
</filter>
<!-- in(A, 1px_mask): out_alpha = A_alpha * mask_alpha; 0 outside 1px → transparent -->
SA-CSS-FCO-002: feComposite operator="out" with inverted full mask
The out operator formula is out_alpha = in1_alpha × (1 - in2_alpha). With a full-element opaque in2 mask (alpha=1 everywhere), output alpha is in1_alpha × (1-1) = 0 — complete erasure. A more sophisticated attack uses a mask that is opaque everywhere except a small transparent "aperture" hole: the consent text survives only through the aperture, making the visible portion appear to be a small window into the full text — visually misleading while most of the consent is invisible.
<filter id="fco2"> <!-- in2: white rect covering full filter region, 4px transparent hole at top-left --> <feFlood flood-color="white" flood-opacity="1" result="full_mask"/> <feComposite in="SourceGraphic" in2="full_mask" operator="out"/> </filter> <!-- out(A, full_opaque): out_alpha = A_alpha * (1-1) = 0 → complete erasure -->
The aperture variant uses an <feImage> referencing a mostly-opaque SVG with a single small transparent region. Only the pixels within the transparent aperture remain visible, focusing user attention on a small, potentially non-consent portion of the element content while the rest is invisible.
SA-CSS-FCO-003: feComposite operator="xor" with near-opaque in2
The xor operator formula is out_alpha = in1_alpha × (1 - in2_alpha) + in2_alpha × (1 - in1_alpha). For two near-opaque layers (in1_alpha ≈ 1 and in2_alpha ≈ 1), this simplifies to: (1 × (1-1)) + (1 × (1-1)) = 0 + 0 = 0. The XOR of two near-opaque images is near-transparent everywhere both layers overlap. Since the consent text has opaque foreground pixels (in1_alpha ≈ 1) and the in2 layer is a solid near-opaque flood (in2_alpha ≈ 1), the output is near-transparent at all text positions. Unlike in and out, xor does not require a geometric mask — it erases through the mathematical property of XOR compositing on opaque layers.
<filter id="fco3"> <feFlood flood-color="white" flood-opacity="0.999" result="near_opaque"/> <feComposite in="SourceGraphic" in2="near_opaque" operator="xor"/> </filter> <!-- xor(~1, ~1): out ≈ (1×0) + (1×0) = 0 → near-transparent → not exactly zero, evades strict alpha=0 checks -->
Detection evasion note: SA-CSS-FCO-003 does not produce output alpha of exactly 0 — with in2_alpha=0.999, output alpha ≈ 0.001. Scanners that only flag alpha=0 will miss this. The effective contrast ratio of alpha=0.001 text on any background approaches 1:1, making the consent text visually absent while technically not fully transparent.
SA-CSS-FCO-004: JS mousedown operator injection (over → in)
Initially, the feComposite uses operator="over" with an in2 set to a transparent layer — in over mode, feComposite alpha-composites in1 over in2; with a transparent in2, in1 is rendered unchanged. The consent text is visible in the initial state. On mousedown, the script changes operator to "in" and simultaneously updates the in2 feImage href to a 1×1 opaque dot. The in compositing immediately erases all but one pixel of the consent text. The user sees the consent text until the click event begins firing, at which point the text disappears.
consentEl.addEventListener("mousedown", () => {
const fc = document.getElementById("consentFilter").querySelector("feComposite");
const fi = document.getElementById("consentFilter").querySelector("feImage");
fi.setAttribute("href", "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'
width='1' height='1'%3E%3Crect width='1' height='1' fill='white'/%3E%3C/svg%3E");
fc.setAttribute("operator", "in"); // erase all but 1px at mousedown
});
Attack summary
| ID | operator | in2 | Formula result | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-FCO-001 | in | 1×1 opaque dot mask | A×0=0 (outside 1px) | 99.99% pixels transparent | Critical |
| SA-CSS-FCO-002 | out | Full opaque mask / aperture | A×(1-1)=0 | Complete erasure / aperture reveal | Critical |
| SA-CSS-FCO-003 | xor | Near-opaque flood (α≈1) | (1×0)+(1×0)≈0 | Near-transparent (evades alpha=0 check) | High |
| SA-CSS-FCO-004 | over→in (mousedown) | Transparent→1px dot | in(A, 1px)=0 outside 1px | Erasure at click time | Critical |
Defences
- Evaluate feComposite in2 mask area: For operator=in, compute the total opaque area of the in2 input. If opaque area < 5% of the element bounding box, flag SA-CSS-FCO-001-class attack. For operator=out, check if in2 is near-fully-opaque (opaque area > 95% of element bbox), indicating near-complete erasure.
- XOR near-opaque detection: For operator=xor, compute the in2 layer average alpha. If average in2 alpha > 0.95 and in1 (consent text) is near-opaque, compute the expected output alpha range. Flag if output alpha < 0.1 for the text glyph positions.
- Simulate filter chain output: Rather than analyzing each primitive independently, simulate the full filter chain pixel output for a representative consent panel input (dark text, white background) and compute WCAG contrast ratio. Flag if contrast ratio < 4.5:1 for normal text, < 3:1 for large text.
- MutationObserver on feComposite operator and feImage href: SA-CSS-FCO-004 changes both. Observe the
operatorattribute of feComposite elements in consent ancestor filters, and thehref/xlink:hrefof feImage elements used as in2 inputs.
SkillAudit findings for this attack surface
feComposite operator="in" + 1×1 feImage mask — in formula: out_alpha = A_alpha × mask_alpha; mask_alpha=0 for 99.99% of consent element area; effective full erasure without exactly-zero output at the 1px opaque point; DOM presence unchanged; textContent intact; getComputedStyle.color unchanged.feComposite operator="out" + full-element opaque feFlood in2 — out formula: out_alpha = A_alpha × (1-1) = 0; complete pixel erasure at all consent text positions; aperture variant selectively reveals small non-consent portion while erasing rest; misleading visual.feComposite operator="xor" + near-opaque feFlood (flood-opacity≈1) — xor of two near-opaque layers ≈ 0; output alpha ≈ 0.001 (not exactly zero); evades exact alpha=0 detection; effective contrast ratio < 1.1:1 on any background; consent text visually absent.feComposite operator from "over" to "in" + feImage href from empty to 1×1 dot — initial state consent text fully visible; click-time erasure; MutationObserver on operator attribute of feComposite and href/xlink:href of in2 feImage elements required.