← Blog

MCP server SVG feDisplacementMap scale consent security

Four SA-CSS-SDP consent attack patterns using the feDisplacementMap scale attribute — controlling displacement magnitude to shift consent text off-element, with JS mousedown injection from identity (scale=0) to high displacement at click time.

By SkillAudit  |  23 Oct 2026

The scale attribute of feDisplacementMap sets the maximum displacement in pixels. The formula is: shift_x = scale × (map_channel_value - 0.5). A channel value of 1 produces a rightward shift of scale/2. A channel value of 0 produces a leftward shift of scale/2. A value of 0.5 (mid-gray) produces zero shift. The scale attribute is the primary lever for consent-bypass attacks using feDisplacementMap: all other parameters (xChannelSelector, displacement map content) determine direction and structure, but scale determines magnitude.

Critical property: feDisplacementMap displacement is a pixel-level rearrangement. The element's getBoundingClientRect is never modified — it always returns the original layout position. textContent is intact. Displaced pixels that exit the filter region are clipped. The consent text can be completely invisible while all accessibility and layout APIs return normal values.

SA-CSS-SDP-001: very large scale with noise/gradient map

With scale="10000" and a displacement map that has any variation from 0.5, every pixel in the consent element is moved far from its original position. A feTurbulence noise map ensures every pixel has a unique displacement value, scrambling glyph strokes into a random scatter pattern. No pixel ends up at its intended position. At scale=10000 and a typical element width of 300px, glyph pixels spread over a region 5000px wide — orders of magnitude outside the visible viewport. The filter region clipping ensures none of this reaches the screen.

<filter id="sdp1" x="-5000%" y="-5000%" width="10000%" height="10000%">
  <feTurbulence type="fractalNoise" baseFrequency="0.05" result="noise"/>
  <feDisplacementMap in="SourceGraphic" in2="noise"
    scale="10000" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<!-- Every pixel scattered ±5000px from original position -->
<!-- Clipped to filter region x/y/width/height if set smaller -->

SA-CSS-SDP-002: scale calibrated to element width

A more precise attack calibrates scale to exactly 2 × element_width. With a solid-alpha displacement map (xChannelSelector="A", alpha=1 throughout), the displacement is exactly scale × 0.5 = element_width. Every pixel shifts exactly one element-width to the right. The consent text is displaced from its starting position to exactly one element-width to the right of its original location — just past the right edge, invisible, but not so far that it looks obviously wrong in a raw attribute check.

A scanner checking only for displacement scale > 1000 would miss a calibrated attack using scale = 600 for a 300px element. The correct threshold is relative: max_displacement = scale/2 must be compared against the element's rendered dimensions, not against an absolute pixel value.

<!-- For element width 300px: scale=600 → shift = 600×0.5 = 300px = exactly off-right-edge -->
<filter id="sdp2">
  <feFlood flood-color="white" flood-opacity="1" result="map"/>
  <feDisplacementMap in="SourceGraphic" in2="map"
    scale="600" xChannelSelector="A" yChannelSelector="A"/>
</filter>

SA-CSS-SDP-003: scale=0 (identity — not an attack)

scale="0" applies zero displacement to all pixels regardless of the displacement map contents. The feDisplacementMap becomes an identity transform — pixels remain at their original positions. This is not a consent-bypass attack.

However, a scanner that flags any feDisplacementMap element on a consent node will produce a false positive here. The correct detection logic requires evaluating the actual maximum displacement: if scale=0, the filter is a no-op. This case is documented explicitly to prevent false-positive alerts that cause alert fatigue and reduce signal for real attacks.

SA-CSS-SDP-004: JS mousedown scale injection (0 → 500)

The filter initially specifies scale="0". The consent text renders normally. A mousedown handler on the consent element (or a transparent overlay) fires on pointer contact and changes scale to "500". With a solid-alpha displacement map, pixels immediately shift 250px to the right. The click registers on the now-displaced consent element. The user's intentional interaction is captured before they have confirmed the consent text is visible.

// SA-CSS-SDP-004: scale 0 → 500 on mousedown
const fd = document.getElementById("consentFilter")
                    .querySelector("feDisplacementMap");
consentElement.addEventListener("mousedown", () => {
  fd.setAttribute("scale", "500");
});

Key detection rule: compute max_displacement = scale / 2; compare against element.getBoundingClientRect().width. If max_displacement ≥ 0.5 × element_width, flag as potential off-element displacement. For the mousedown variant, add scale to the MutationObserver attribute list on all feDisplacementMap elements in consent ancestor filter chains.

Attack summary

IDscale valueMap typeEffectSeverity
SA-CSS-SDP-00110000feTurbulence noiseAll pixels scattered; text invisibleCritical
SA-CSS-SDP-0022 × element_widthSolid alpha=1Text shifted exactly one width to the rightCritical
SA-CSS-SDP-0030AnyIdentity — no effect; not an attackMedium (FP risk)
SA-CSS-SDP-0040 → 500 (mousedown)Solid alpha=1Text displaced 250px right at click timeCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-SDP-001: feDisplacementMap scale very large (e.g., 10000) + feTurbulence in2 map — all pixels scattered ±scale/2 pixels from origin; consent text fully dissolved; filter region clips scattered pixels; BCR unchanged; textContent intact.
CRITICAL SA-CSS-SDP-002: scale = 2 × element_width + solid-alpha feFlood in2 + xChannelSelector="A" — deterministic shift of exactly element_width pixels; consent text displaced off right edge; evades absolute-threshold checks; detection requires ratio comparison vs element BCR.
MEDIUM SA-CSS-SDP-003: scale="0" — identity transform; no displacement effect; not an attack; document to prevent false-positive alerts on legitimate feDisplacementMap uses.
CRITICAL SA-CSS-SDP-004: JS mousedown changes scale from 0 to 500 — initial render normal (identity); text displaced 250px right at click time; MutationObserver on scale attribute of all feDisplacementMap elements in consent-ancestor filters required.

Related: xChannelSelector/yChannelSelector attacks  |  feDisplacementMap general attacks  |  feConvolveMatrix order attacks  |  targetX/targetY boundary attacks

← Blog  |  Security Checklist