MCP server SVG feDisplacementMap xChannelSelector consent security
Four SA-CSS-XCS consent attack patterns using feDisplacementMap xChannelSelector/yChannelSelector attributes to drive pixel displacement from the alpha or colour channels of a co-rendered displacement map — shifting consent text off-element without affecting layout metrics.
feDisplacementMap displaces each pixel of its first input (in) by an amount read from the same-position pixel in a second input (in2, the displacement map). The x-displacement uses the channel specified by xChannelSelector; y-displacement uses yChannelSelector. Valid values are R, G, B, and A. The displacement formula is: shift_x = scale × (channel_value - 0.5). A channel value of 0 produces a shift of -scale/2; a value of 1 produces +scale/2; a value of 0.5 (mid-gray) produces zero shift.
Attack class: feDisplacementMap attacks displace pixels rather than recolor them. getBoundingClientRect returns the original layout dimensions. textContent is intact. getComputedStyle shows no anomalies. The consent text may be entirely outside the element's bounding box, yet all DOM/CSSOM checks pass.
SA-CSS-XCS-001: alpha-channel displacement (xChannelSelector="A")
When xChannelSelector="A", the x-displacement of each pixel is controlled by the alpha channel of the displacement map at that pixel's location. If the displacement map is a solid-color image with uniform alpha=1 throughout (e.g., generated by a feFlood with any opaque color), every pixel receives the same displacement: scale × (1 - 0.5) = scale × 0.5 = scale/2 pixels to the right.
For scale="400", every pixel in the consent text element is displaced 200px to the right. If the element is 300px wide, all glyph pixels land at x > 200 relative to the element's left edge — outside the element's visual region if the filter region clips at the element boundary. The consent text becomes invisible. The bounding box remains at the original position; click events fire. An attacker using scale="2 × element_width" can guarantee full off-element displacement for any element width.
<filter id="xcs1" x="0" y="0" width="1" height="1">
<feFlood flood-color="white" flood-opacity="1" result="map"/>
<feDisplacementMap in="SourceGraphic" in2="map"
scale="400" xChannelSelector="A" yChannelSelector="A"/>
</filter>
<!-- Solid white map: alpha=1 everywhere; shift = 400×0.5 = 200px right and down -->
<!-- BCR unchanged; textContent intact; consent text outside filter region -->
SA-CSS-XCS-002: colour-channel structured displacement (xChannelSelector="R"/"G"/"B")
When xChannelSelector is set to a colour channel, each pixel's x-displacement depends on that channel's value in the displacement map at that pixel's position. If the displacement map encodes a horizontal gradient (brighter on the right), displacement increases toward the right edge — each column of glyph pixels shifts by a different amount. The result is that glyph strokes are scrambled: each vertical column of pixels moves to a different x position. The consent text becomes unreadable (characters warped beyond recognition) without becoming transparent.
For a scanner that checks for transparency or off-screen displacement but not glyph scrambling, this variant passes. The text is technically present at its declared position in the DOM; it is just visually indecipherable to a human reader.
<filter id="xcs2">
<feFlood flood-color="#ff0000" result="red"/>
<feImage href="data:image/svg+xml,..." result="gradient_map"/>
<feDisplacementMap in="SourceGraphic" in2="gradient_map"
scale="60" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<!-- Each pixel row shifts by a different amount; glyphs are distorted column-by-column -->
SA-CSS-XCS-003: dual-axis alpha displacement (both X and Y driven by alpha)
Setting both xChannelSelector="A" and yChannelSelector="A" with a solid-alpha displacement map displaces every pixel by (scale/2, scale/2) — a uniform diagonal shift. For scale="600" with a standard consent panel, the consent text shifts 300px right and 300px down. If the filter region is bounded to the element (default SVG filter region covers the element bounding box plus margin), text shifted beyond the filter region is clipped and disappears. All DOM values remain normal.
<filter id="xcs3" x="-50%" y="-50%" width="200%" height="200%">
<feFlood flood-color="white" result="map"/>
<feDisplacementMap in="SourceGraphic" in2="map"
scale="600" xChannelSelector="A" yChannelSelector="A"/>
</filter>
<!-- Shift: (300px right, 300px down). Consent text outside standard viewport. -->
SA-CSS-XCS-004: JS mousedown channel injection
The initial filter state uses xChannelSelector="R" with a neutral (zero-displacement) displacement map that contains a 0.5 mid-gray R channel. The consent text renders normally. On mousedown, a script changes xChannelSelector to "A" and simultaneously updates the displacement map to a solid-alpha feFlood. At the moment the user's pointer contacts the consent dialog, text shifts off-element. The click registers on the now-invisible consent element below the cursor. The attack completes before the user's intentional click decision is finalized.
element.addEventListener("mousedown", () => {
const fedm = document.getElementById("consentFilter")
.querySelector("feDisplacementMap");
fedm.setAttribute("xChannelSelector", "A");
fedm.setAttribute("yChannelSelector", "A");
fedm.setAttribute("scale", "400");
// simultaneously update in2 map to solid-white feFlood (alpha=1)
});
Detection gap: Most MutationObserver implementations on consent filters watch for kernelMatrix, stdDeviation, or opacity-adjacent attributes. xChannelSelector, yChannelSelector, and scale on feDisplacementMap are rarely included. This is a known monitoring gap.
Attack summary
| ID | xChannelSelector | Map type | Effect | Severity |
|---|---|---|---|---|
| SA-CSS-XCS-001 | A | Solid alpha=1 (feFlood) | All pixels shift scale/2 right; text off-element | Critical |
| SA-CSS-XCS-002 | R/G/B | Gradient map | Column-by-column shifts; glyphs scrambled/unreadable | High |
| SA-CSS-XCS-003 | A + yChannelSelector=A | Solid alpha=1 | Diagonal shift; text outside filter region → clipped | Critical |
| SA-CSS-XCS-004 | R→A (mousedown) | Neutral→solid alpha | Text shifts off-element at click time | Critical |
Defences
- Evaluate maximum displacement at runtime: For every
feDisplacementMapin a consent ancestor filter, compute the maximum possible displacement usingscaleand the selected channel range. ForxChannelSelector="A"with opaque displacement map, max_shift =scale × 0.5. Flag if max_shift exceeds 50% of the element width. - Audit the displacement map source: If the in2 input is a solid-color
feFloodwith non-zero alpha andxChannelSelector="A", the displacement is deterministic and maximum. Flag this combination unconditionally. - Check for glyph scrambling via canvas simulation: Render the consent element with and without the filter; compare glyph positions via canvas pixel comparison. If glyph centroids move by more than 10px between the two renders, flag as potential scrambling attack.
- MutationObserver on
xChannelSelector,yChannelSelector, andscalefor all feDisplacementMap elements in consent ancestor filter chains. These attributes are not commonly monitored but are the critical inputs for this attack class.
SkillAudit findings for this attack surface
xChannelSelector="A" + solid-alpha feFlood in2 map + large scale — deterministic max displacement; all pixels shift right by scale/2; consent text off-element; BCR unchanged; textContent intact.xChannelSelector="R"/"G"/"B" + gradient displacement map — column-by-column shifts; consent glyph pixels scrambled; text unreadable without being transparent; passes transparency-only scanners.xChannelSelector="A" + yChannelSelector="A" + solid-alpha map — diagonal shift (scale/2, scale/2); entire consent text block moves outside filter region → clipped; invisible; all DOM checks pass.xChannelSelector from "R" to "A" — initial render normal; text shifts off-element at click time; MutationObserver must include xChannelSelector, yChannelSelector, scale on feDisplacementMap elements.