SVG Filter Compositing Attacks on MCP Consent: feComposite Operator, feMerge Node Ordering, and Filter Region Abuse
Three SVG filter architecture families attack MCP consent visibility from orthogonal angles — Porter-Duff compositing, primitive sequence layering, and geometric application boundary — and why a single output-simulation check is the only detection strategy that covers all three.
When we audit MCP server SVG filters for consent attacks at SkillAudit, we group attacks by what aspect of the filter system they exploit. Some attacks target individual primitive attributes — wrong blur sigma, extreme displacement scale, malicious color matrix. These are attribute-level attacks and they can in principle be caught by per-attribute rules.
But three attack families exploit a different layer: the architectural rules of how SVG filters themselves are composed and applied. These are structural attacks. They exploit the Porter-Duff compositing formula (feComposite operator), the layering order of parallel primitive outputs (feMerge node ordering), and the geometric specification of where a filter applies (filter region attributes). Together they define a set of consent evasion paths that no per-attribute rule can catch — because the attack is not in any single attribute value but in the combination, sequence, or boundary specification.
This post synthesizes the twelve SA-CSS patterns across these three families from our recent research (sessions covering SA-CSS-FCO, SA-CSS-FMO, and SA-CSS-FRG patterns). We cover each family's mathematical basis, the specific attack patterns, and then the one check that unifies detection across all three.
Family 1: feComposite operator — Porter-Duff as mask erasure
The feComposite primitive composites two images using a Porter-Duff compositing formula selected by the operator attribute. The relevant operators for consent attacks are in, out, and xor. Unlike blending modes, which mix color channels, these operators work primarily on the alpha channel using explicit geometric masking formulas.
in-operator formula: out_alpha = in1_alpha × in2_alpha. When in2 is a mask image with alpha=1 only at a 1×1 pixel and alpha=0 everywhere else, every output pixel has alpha = in1_alpha × 0 = 0 except the single unmasked pixel. A consent panel of 400×80 pixels contains 32,000 pixels; a 1×1 mask erases 31,999 of them. DOM presence, textContent, BCR, and getComputedStyle are unchanged. The element is invisible in rendering.
SA-CSS-FCO-001: operator="in" with 1×1 feImage mask
A feImage referencing a 1×1 white-pixel data URI serves as the in2 mask. The in-compositing formula out_alpha = in1_alpha × in2_alpha zeros every pixel where in2_alpha=0, which is 99.99% of the element. The single opaque pixel of the mask allows exactly one element pixel to survive.
<filter id="fco1">
<feImage href="data:image/svg+xml,..." result="tiny_mask"
x="0" y="0" width="1" height="1"/>
<feComposite in="SourceGraphic" in2="tiny_mask" operator="in"/>
</filter>
SA-CSS-FCO-002: operator="out" with full-element opaque mask
The out formula is out_alpha = in1_alpha × (1 - in2_alpha). A full-element opaque feFlood as in2 (alpha=1 everywhere) makes every output pixel: in1 × (1-1) = 0 — complete erasure. A variant uses a mostly-opaque mask with a small transparent aperture, selectively revealing only a non-consent portion of the element.
SA-CSS-FCO-003: operator="xor" with near-opaque in2
The xor formula is in1×(1-in2) + in2×(1-in1). For two near-opaque layers (in1≈1, in2≈1): (1×0) + (1×0) ≈ 0. The output alpha is near-zero. With in2_alpha=0.999, the output alpha ≈ 0.001 — not exactly zero, evading alpha=0 detection, but with effective contrast ratio < 1.1:1 making consent text visually absent.
SA-CSS-FCO-004: JS mousedown operator injection (over → in)
Initial operator=over with transparent in2 renders consent text fully. At mousedown, operator changes to "in" and a 1×1 mask is injected as in2. Consent text erases at the exact moment of clicking. MutationObserver on the operator attribute and href of in2 feImage is required.
Family 2: feMerge node ordering — last node always wins
feMerge composites multiple primitive outputs by alpha-compositing them in document order: each feMergeNode is rendered over all preceding nodes. This is equivalent to a sequential series of over-compositing operations. The invariant: the last feMergeNode in document order determines what covers all earlier layers. An opaque solid-color node at the end of the feMerge stack overwrites everything before it.
Last-node property: For feMerge with N nodes, the output is over(node_{N-1}, over(node_{N-2}, ... over(node_1, node_0) ...)). If node_{N-1} is a fully opaque white feFlood, the entire output is white regardless of nodes 0 through N-2. A scanner that examines each feMergeNode independently — checking whether each node produces harmful output — will not detect that a harmless-looking final node dominates the entire output.
SA-CSS-FMO-001: SourceGraphic first + opaque white feFlood last
feMerge with SourceGraphic as the first node and a solid white feFlood as the final node. The over-stack puts white on top of everything: over(SourceGraphic, white) = white. The consent text is present as the first node but completely obscured by the final white layer. The filter appears to contain the consent element (SourceGraphic is there) but the final output is opaque white.
<filter id="fmo1">
<feFlood flood-color="white" flood-opacity="1" result="white_layer"/>
<feMerge>
<feMergeNode in="SourceGraphic"/> <!-- visible layer -->
<feMergeNode in="white_layer"/> <!-- final node: covers everything -->
</feMerge>
</filter>
SA-CSS-FMO-002: Multiple benign intermediate nodes + white final node
A more evasion-resistant variant uses 3–5 benign intermediate nodes (the consent source, a slight blur, a color adjustment, a drop shadow) followed by the white final node. Per-node analysis examines each node and finds all intermediate nodes produce reasonable output. Only the final node — which produces white — determines the actual rendered result. The attack exploits the assumption that a complex, multi-step filter chain is more trustworthy than a simple one.
SA-CSS-FMO-003: Ordering matters — this is NOT an attack
For completeness: feMerge with white feFlood first and SourceGraphic last is not a consent attack. The over-stack puts SourceGraphic on top: over(white, SourceGraphic) renders the consent text normally over the white flood. The flood is below the source. This ordering is benign and detectors must correctly distinguish first-node-white (benign) from last-node-white (attack).
SA-CSS-FMO-004: JS mousedown appends white feMergeNode
Initially, feMerge contains only SourceGraphic. At mousedown, a new feMergeNode referencing a white feFlood is appended to the feMerge element. The appended node becomes the new final (top) layer, instantly white-outing the consent text. MutationObserver on the feMerge element's childList (not just attribute changes) is required to catch this — the attack involves node insertion, not attribute mutation.
consentEl.addEventListener("mousedown", () => {
const feMerge = document.querySelector("#f feMerge");
const node = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
node.setAttribute("in", "white_layer");
feMerge.appendChild(node); // appended = new top layer = white-out
});
Family 3: Filter region attributes — the zero-area transparent rule
A filter's application region is specified by the x, y, width, and height attributes of the <filter> element (defaulting to x="-10%" y="-10%" width="120%" height="120%"). The SVG specification states: if the filter region has zero area (width=0 or height=0), the filter produces fully transparent output. This is not an edge case or undefined behavior — it is an explicit, mandated specification requirement. The transparent rule applies regardless of the filter's primitive chain: even a perfectly benign identity filter produces transparent output if its region has zero area.
Zero-area transparent rule (SVG spec §13.7): "If the filter region is zero in area, then the result is an empty image." This is determined before any primitive is evaluated. An attacker needs only width="0" on the <filter> element to make the entire consent panel transparent — no primitive manipulation required. A scanner that analyzes the primitive chain in detail while skipping the filter element attributes will miss the most minimal consent evasion possible: a two-attribute attack.
SA-CSS-FRG-001: filter width="0" or height="0" — zero-area transparent
The most minimal consent attack in the SVG filter attack taxonomy. A <filter width="0"> applied to any consent element produces a fully transparent output — not because of any primitive, but because of the SVG zero-area rule applied before any primitive executes. The primitive chain can be completely benign. Static analysis of primitives produces zero findings; only analysis of the filter element attributes reveals the attack.
<filter id="frg1" width="0"> <!-- any primitives here; they are never evaluated --> <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/> </filter> <!-- zero-area → transparent output; primitive chain irrelevant -->
SA-CSS-FRG-002: Partial region — selective quadrant erasure
Setting the filter region to cover only part of the element (e.g., x="50%" y="50%" width="50%" height="50%") applies the filter only to the bottom-right quadrant. The filter chain applied to that quadrant can be a white-out or transparency attack, erasing only the portion of the consent text that falls there while the other 75% of the element renders normally. This attack hides legally significant content — such as the final sentence of a consent clause — by targeting a specific quadrant where it typically appears.
SA-CSS-FRG-003: Expanded region for displacement setup
Setting an extremely expanded filter region (x="-5000%" y="-5000%" width="10100%" height="10100%") captures off-element displaced pixels. Without this expanded region, a large-scale feDisplacementMap would clip displaced pixels at the default filter boundary. With the expanded region, pixels displaced ±500px from the element boundary are still captured in the filter output — enabling large-scale displacement attacks that would otherwise be clipped to within the element bounds.
SA-CSS-FRG-004: JS mousedown sets width from "120%" to "0%"
Initial filter width="120%" (standard). At mousedown, width is set to "0%". The zero-area transparent rule triggers, instantly making the consent element transparent. The primitive chain remains unchanged — only the filter region changes. This is an extremely minimal JS attack: one setAttribute call on the filter element produces complete consent erasure.
consentEl.addEventListener("mousedown", () => {
document.querySelector("#consentFilter")
.setAttribute("width", "0"); // zero-area → instant transparent
});
Why these three families require a unified detection strategy
These families exploit three different architectural layers of SVG filter processing:
| Family | Layer exploited | What per-family analysis checks | What it misses |
|---|---|---|---|
| feComposite operator (FCO) | Pixel-level alpha arithmetic | operator attribute + in2 alpha properties | feMerge ordering, filter region |
| feMerge ordering (FMO) | Primitive sequence structure | Final feMergeNode input type | feComposite operators, filter region |
| Filter region (FRG) | Geometric application boundary | width/height attributes on filter element | feComposite operators, feMerge ordering |
An attacker combining all three in a single filter — a non-zero-area region (passing FRG check), a non-out/in/xor operator (passing FCO check), and a non-final-white feMerge (passing FMO check) — while using, say, a large-scale displacement attack via feTurbulence, would evade all three per-family scanners. The only strategy that catches all of them is: simulate the filter chain output and measure its effect on the consent element's visible contrast ratio.
Detection algorithm: For every filter applied to an ancestor of any detected consent element, synthesize a test input (dark text on white background, representative of the consent element content), apply the filter chain via an OffscreenCanvas or headless rendering pass, and compute the WCAG contrast ratio between the dark foreground pixels and the background in the output. If contrast ratio < 4.5:1 for normal text size, flag the filter regardless of which architectural layer produced the result.
Interaction between families: combined attacks
The three families combine. Consider this filter that would evade per-family analysis:
<!-- Evasion of all three per-family scanners -->
<filter id="combined" x="-10%" y="-10%" width="120%" height="120%">
<!-- FCO check: operator="over" = identity, passes -->
<feComposite in="SourceGraphic" in2="SourceGraphic" operator="over" result="pass"/>
<!-- feTurbulence with pre-computed seed for max-displacement at element center -->
<feTurbulence type="turbulence" baseFrequency="0.05" numOctaves="1"
seed="12847" result="noise"/>
<feDisplacementMap in="pass" in2="noise" scale="400" xChannelSelector="R"/>
<!-- FMO check: single feMergeNode, no white final node, passes -->
</filter>
<!-- All three per-family checks pass. But output: consent text displaced 200px off-element. -->
This example uses none of the three structural attack families directly — it uses a pre-computed seed displacement attack (SA-CSS-TSE-001) that evades all three structural scanners. The lesson is that output-simulation detection is not just a fallback for when per-family analysis fails; it is the only first-class check that covers the entire attack space.
MutationObserver coverage for click-time attacks
Four of the twelve patterns (SA-CSS-FCO-004, SA-CSS-FMO-004, SA-CSS-FRG-004, and similar JS-inject variants) change the filter state at mousedown. These require dynamic detection via MutationObserver:
| Pattern | What changes | Observer target |
|---|---|---|
| SA-CSS-FCO-004 | feComposite operator attribute; feImage href attribute | feComposite.operator; feImage.href |
| SA-CSS-FMO-004 | New feMergeNode appended to feMerge | feMerge childList |
| SA-CSS-FRG-004 | filter width attribute changed to "0" | filter.width; filter.height |
The observer scope is: all <filter> elements that are referenced by any ancestor of any detected consent element (traversed upward from the consent element through the DOM ancestry chain). Changes to filter elements not in the ancestry chain are not relevant and should not generate alerts.
Full pattern reference table
| ID | Family | Mechanism | SVG layer | JS? | Severity |
|---|---|---|---|---|---|
| SA-CSS-FCO-001 | feComposite | operator=in + 1×1 mask → 99.99% erasure | Pixel alpha arithmetic | No | Critical |
| SA-CSS-FCO-002 | feComposite | operator=out + full-element mask → complete erasure | Pixel alpha arithmetic | No | Critical |
| SA-CSS-FCO-003 | feComposite | operator=xor + near-opaque in2 → near-zero (evades alpha=0 check) | Pixel alpha arithmetic | No | High |
| SA-CSS-FCO-004 | feComposite | operator=over→in + tiny mask at mousedown | Pixel alpha arithmetic | mousedown | Critical |
| SA-CSS-FMO-001 | feMerge | SourceGraphic first + white last → white-out | Primitive sequence | No | Critical |
| SA-CSS-FMO-002 | feMerge | Multiple benign nodes + white final → white-out | Primitive sequence | No | Critical |
| SA-CSS-FMO-004 | feMerge | Append white feMergeNode at mousedown | Primitive sequence | mousedown | Critical |
| SA-CSS-FRG-001 | Filter region | width="0" → SVG zero-area transparent rule | Geometric boundary | No | Critical |
| SA-CSS-FRG-002 | Filter region | Partial region → applies attack only to one quadrant | Geometric boundary | No | High |
| SA-CSS-FRG-003 | Filter region | Expanded region → displacement attack beyond default clip | Geometric boundary | No | High |
| SA-CSS-FRG-004 | Filter region | width="120%"→"0" at mousedown → instant transparent | Geometric boundary | mousedown | Critical |
Conclusions
The three filter compositing attack families described here are unusual in the SVG consent attack taxonomy because they exploit architectural rules rather than attribute values. feComposite operator attacks use the Porter-Duff compositing formula as a mask erasure tool. feMerge ordering attacks use the last-node-wins layering property. Filter region attacks use the SVG zero-area transparent rule. Each family is invisible to detectors focused on the other two.
The practical implication for MCP skill auditors: do not build a scanner that is a collection of per-pattern attribute checks. Build a scanner that simulates filter output on a representative consent element and measures the contrast ratio of the result. A scanner that does that one check correctly catches all twelve patterns described here, plus all attribute-level attacks (extreme blur sigma, bias=1, displacement scale=10000), without requiring explicit pattern knowledge for any of them.
SkillAudit's audit engine implements exactly this output-simulation approach as the primary consent-filter detector. Every filter applied to an ancestor of any detected consent element is simulated on a dark-text-on-white test input, and the output contrast ratio is measured. This is what a SkillAudit audit report flags under the SA-CSS category — the reported severity reflects what the filter actually does to consent visibility, not what its attributes appear to say.