MCP server SVG feMerge node ordering consent security
Four SA-CSS-FMO consent attack patterns exploiting the feMerge bottom-to-top compositing order — placing a solid-white layer as the last feMergeNode overwrites all earlier layers including consent text — with JS mousedown node append for click-time white-out.
The feMerge SVG filter primitive composites an ordered list of images using the SVG over compositing operator. The W3C SVG specification states: "This filter primitive composites input image layers on top of each other using the over operator." Nodes are composited in document order — the first feMergeNode is the bottom layer, the last is the top layer. This is the direct SVG equivalent of CSS z-index layering for filter inputs. An attacker who controls any content that appears in the SVG filter definition for a consent element can append a final feMergeNode referencing a solid opaque white layer — this single node, placed last, covers all earlier layers and produces a solid white output regardless of what the earlier nodes contribute.
feMerge over composition property: For feMerge with final node F (fully opaque white, alpha=1), the over formula is: out = F + prev_layer × (1 - F_alpha) = white + prev × 0 = white. The final node completely dominates all previous nodes. This means a single appended feMergeNode is sufficient to white-out any consent text regardless of the complexity of the preceding filter chain — the attacker does not need to understand or modify the rest of the filter.
SA-CSS-FMO-001: Final white feMergeNode white-out
The minimal attack form: a feMerge with exactly two feMergeNode children — the first referencing SourceGraphic (consent text, bottom layer) and the second referencing a solid white feFlood result (top layer). The feFlood is defined earlier in the filter chain with flood-color="white" and flood-opacity="1". The over compositing at the second node produces solid white output. The consent element remains fully present in the DOM; textContent is unchanged; all event handlers fire normally; the element is interactable — only the visual output is white-on-white invisible.
<filter id="fmo1">
<feFlood flood-color="white" flood-opacity="1" result="white_layer"/>
<feMerge>
<feMergeNode in="SourceGraphic"/> <!-- bottom: consent text -->
<feMergeNode in="white_layer"/> <!-- top: solid white → covers everything -->
</feMerge>
</filter>
<!-- feMerge over(bottom, top): top white_layer (alpha=1) dominates; output = white -->
SA-CSS-FMO-002: Obfuscated multi-stage merge with terminal white node
An obfuscated variant that inserts multiple legitimate-looking intermediate processing steps as earlier feMergeNode inputs: a feGaussianBlur result (blurred copy of source), a feColorMatrix desaturated copy, a feOffset shifted copy — followed by the final solid-white feMergeNode. Reviewers analyzing the filter primitive-by-primitive may focus on whether feGaussianBlur, feColorMatrix, and feOffset individually affect readability (they do not critically), while missing that the final white node unconditionally dominates all of them. The attack lives entirely in the ordering of the last node, not in any individual primitive's parameters.
<filter id="fmo2">
<feGaussianBlur stdDeviation="0.5" result="slight_blur"/>
<feColorMatrix type="saturate" values="0.9" in="SourceGraphic" result="desaturated"/>
<feOffset dx="0" dy="0" in="SourceGraphic" result="shifted"/>
<feFlood flood-color="white" flood-opacity="1" result="white_overlay"/>
<feMerge>
<feMergeNode in="slight_blur"/>
<feMergeNode in="desaturated"/>
<feMergeNode in="shifted"/>
<feMergeNode in="white_overlay"/> <!-- final top node: white-out regardless of earlier nodes -->
</feMerge>
</filter>
Per-primitive analysis is insufficient: SA-CSS-FMO-002 cannot be detected by analyzing each primitive in isolation. Detection requires ordering-aware analysis of the feMerge node list — specifically, checking whether the last feMergeNode input is (or resolves to) a high-luminance, near-fully-opaque layer.
SA-CSS-FMO-003: Correct form — SourceGraphic last (non-attack, FP prevention)
The non-attack form for comparison: feMerge with a white feFlood as the first node and SourceGraphic as the last node. Since SourceGraphic is on top, the consent text is visible over the white background. This is the expected pattern when using feMerge to composite a background behind an element. Scanners must not flag this pattern. The ordering criterion is: flag when the last node is a high-luminance, near-fully-opaque flood or image that covers the full filter region. Do not flag when the first nodes are solid layers and SourceGraphic follows as the final node.
<filter id="fmo3-safe">
<feFlood flood-color="white" flood-opacity="1" result="bg"/>
<feMerge>
<feMergeNode in="bg"/> <!-- bottom: white background -->
<feMergeNode in="SourceGraphic"/> <!-- top: consent text on top → VISIBLE -->
</feMerge>
</filter>
<!-- SourceGraphic is last (top): consent text visible. NOT an attack. -->
SA-CSS-FMO-004: JS mousedown feMergeNode append
Initially, the feMerge has only one node: SourceGraphic. The feFlood producing the white layer exists in the filter chain but its result is not referenced by any feMergeNode. The consent text is visible. On mousedown, the script appends a new feMergeNode element (created via document.createElementNS) with in="white_layer" to the existing feMerge element. The new node becomes the top layer and immediately produces white-out. This attack is particularly difficult to detect statically — in the initial state, the filter has no attack-pattern structure (the white feFlood result is simply an unused intermediate). The attack only activates at runtime.
consentEl.addEventListener("mousedown", () => {
const feMergeEl = filter.querySelector("feMerge");
const newNode = document.createElementNS("http://www.w3.org/2000/svg", "feMergeNode");
newNode.setAttribute("in", "white_layer"); // references pre-existing feFlood result
feMergeEl.appendChild(newNode); // appended node becomes top layer → white-out
});
Attack summary
| ID | feMerge structure | Last node | Effect | Severity |
|---|---|---|---|---|
| SA-CSS-FMO-001 | 2 nodes: SourceGraphic, white_layer | white_layer (opaque white) | Complete white-out | Critical |
| SA-CSS-FMO-002 | 4 nodes: blur, desaturated, shifted, white_overlay | white_overlay (opaque white) | Complete white-out (obfuscated) | High |
| SA-CSS-FMO-003 | 2 nodes: bg, SourceGraphic | SourceGraphic | Consent text visible — NOT attack | Benign |
| SA-CSS-FMO-004 | 1→2 nodes at mousedown | Appended white_layer (mousedown) | White-out at click time | Critical |
Defences
- Check final feMergeNode input luminance and opacity: For each feMerge in a consent ancestor filter chain, resolve the input of the last feMergeNode to its source primitive. If the source is a feFlood with luminance > 0.9 and flood-opacity = 1 (or near-1), flag SA-CSS-FMO-001-class attack unconditionally.
- Ordering-aware merge analysis: Do not analyze feMergeNode inputs in isolation. Analyze the ordered sequence. A white feFlood as the first node is benign if SourceGraphic follows. A white feFlood as the last node is always an attack regardless of how many legitimate nodes precede it.
- Resolve named result references: For SA-CSS-FMO-002, the feMergeNode input names reference result strings from earlier primitives. Resolve each name to its source primitive before evaluating. The attacker may give the white feFlood a non-obvious result name (e.g., result="processed_output") to obscure its nature.
- MutationObserver on feMerge childList: SA-CSS-FMO-004 appends a new child node. MutationObserver with
childList: trueon feMerge elements in consent ancestor filters detects appended feMergeNode elements. Re-evaluate the merge ordering on every childList mutation.
SkillAudit findings for this attack surface
feMerge with SourceGraphic as first node and solid opaque white feFlood result as final node — over compositing: white (alpha=1) on top of SourceGraphic; output = white for all pixels; complete consent text white-out; element interactable; textContent unchanged; getComputedStyle.color unchanged.feMerge with 3 benign intermediate nodes (blur, colormatrix, offset) followed by solid white final node — per-primitive analysis of intermediate nodes does not reveal attack; final node white-out dominates all; ordering-aware last-node luminance check required; evades primitive-list scanners that don't track node order.feMergeNode in="white_layer" to existing feMerge — initial state: single SourceGraphic node (consent visible); appended white_layer node becomes top layer at mousedown; white-out at click time; static analysis of initial filter state misses attack; MutationObserver on feMerge childList required.