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.
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
| ID | scale value | Map type | Effect | Severity |
|---|---|---|---|---|
| SA-CSS-SDP-001 | 10000 | feTurbulence noise | All pixels scattered; text invisible | Critical |
| SA-CSS-SDP-002 | 2 × element_width | Solid alpha=1 | Text shifted exactly one width to the right | Critical |
| SA-CSS-SDP-003 | 0 | Any | Identity — no effect; not an attack | Medium (FP risk) |
| SA-CSS-SDP-004 | 0 → 500 (mousedown) | Solid alpha=1 | Text displaced 250px right at click time | Critical |
Defences
- Compute relative displacement:
max_displacement = scale/2. Compare againstelement_width(from BCR). Flag if ratiomax_displacement / element_width > 0.5— text shifted more than half its own width is likely an attack. - Do not flag scale=0: This is an identity transform. Flag only when scale is non-zero AND the resulting displacement exceeds the threshold above.
- Evaluate displacement map uniformity: If in2 is a solid feFlood (all pixels identical), displacement is uniform and deterministic. Compute it exactly. If in2 is feTurbulence or an external image, simulate displacement on a sample of pixel positions.
- MutationObserver on
scale: SA-CSS-SDP-004 changes only thescaleattribute. Addscaleto the observer attribute list alongsidexChannelSelector,yChannelSelector, andin2for all feDisplacementMap elements in consent ancestor filter chains.
SkillAudit findings for this attack surface
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.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.scale="0" — identity transform; no displacement effect; not an attack; document to prevent false-positive alerts on legitimate feDisplacementMap uses.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.