← Blog

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.

By SkillAudit  |  23 Oct 2026

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

IDxChannelSelectorMap typeEffectSeverity
SA-CSS-XCS-001ASolid alpha=1 (feFlood)All pixels shift scale/2 right; text off-elementCritical
SA-CSS-XCS-002R/G/BGradient mapColumn-by-column shifts; glyphs scrambled/unreadableHigh
SA-CSS-XCS-003A + yChannelSelector=ASolid alpha=1Diagonal shift; text outside filter region → clippedCritical
SA-CSS-XCS-004R→A (mousedown)Neutral→solid alphaText shifts off-element at click timeCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-XCS-001: 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.
HIGH SA-CSS-XCS-002: xChannelSelector="R"/"G"/"B" + gradient displacement map — column-by-column shifts; consent glyph pixels scrambled; text unreadable without being transparent; passes transparency-only scanners.
CRITICAL SA-CSS-XCS-003: 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.
CRITICAL SA-CSS-XCS-004: JS mousedown changes xChannelSelector from "R" to "A" — initial render normal; text shifts off-element at click time; MutationObserver must include xChannelSelector, yChannelSelector, scale on feDisplacementMap elements.

Related: feDisplacementMap scale attacks  |  feDisplacementMap general attacks  |  feImage href attacks  |  feConvolveMatrix order attacks

← Blog  |  Security Checklist