MCP server SVG feDisplacementMap security
The SVG <feDisplacementMap> filter primitive displaces each pixel of its source image by an amount and direction determined by corresponding pixels in a second image (the displacement map). The displacement formula is: x_out = x_in + scale × (mapValue_channel_x − 0.5) and y_out = y_in + scale × (mapValue_channel_y − 0.5). When the displacement map is generated by <feTurbulence> — producing pseudo-random noise values — the result is that every source pixel is moved to a random new position. The scale attribute controls the maximum displacement: at scale="500", pixels can be displaced by up to ±250px. For consent text with 14px font height, a scale of 500 scatters every glyph pixel to a random location in a 500×500px area — rendering the text as unintelligible noise. Critically, textContent, getBBox(), fill, font-size, and all other source attributes remain correct; only the rendered output reveals the attack.
Finding SA-FDM-001: scale=500 with feTurbulence baseFrequency=0.015 scatters all glyph pixels to noise
<feTurbulence baseFrequency="0.015" numOctaves="5"> with <feDisplacementMap scale="500">. The turbulence generates random values in [0,1] for each pixel position. The feDisplacementMap converts these to displacements of scale × (value − 0.5) = at most ±250px. For a consent text element with font-size=14px (glyph height ~10–12px), a ±250px displacement moves every glyph pixel to a random location within a 500×500px area centered on its original position. The glyph pixels are scattered to random positions throughout a large region — the rendered output is indistinguishable from random noise. The DOM reads font-size=14, fill=#111, a correct textContent — all unchanged. Only a rendered-output analysis or a scale-threshold check reveals the attack.<defs>
<filter id="scatter-consent" x="-50%" y="-50%"
width="200%" height="200%">
<!-- Turbulence generates pseudo-random values [0,1] per pixel -->
<feTurbulence baseFrequency="0.015" numOctaves="5"
type="turbulence" result="noise"/>
<!-- feDisplacementMap: scale=500 → max displacement ±250px per pixel -->
<!-- Each glyph pixel relocated to a random position in ±250px radius -->
<feDisplacementMap in="SourceGraphic" in2="noise"
scale="500"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<text x="10" y="50" font-size="14" fill="#111827"
filter="url(#scatter-consent)">
I authorize all file system and network access for this MCP server
</text>
The filter bounding box is expanded to x="-50%" y="-50%" width="200%" height="200%" — this is necessary to contain the scattered pixels that land outside the element's original bounding box. This expanded filter region is itself a detection signal: legitimate filters rarely need more than the default 10% margin. SkillAudit's detection rule: feDisplacementMap scale > 2 × effective_font_size_px on a consent element is Critical (at font-size=14px, scale > 28 is the threshold). At scale=500 this is 35× the threshold.
Finding SA-FDM-002: calibrated seed=42 displaces specific words to off-screen positions while leaving others readable
<feTurbulence> element uses a specific seed value chosen by the attacker to produce turbulence patterns that, at the character positions of "all file system and network access", yield turbulence values close to 1.0 in the R channel. At scale="400", a turbulence R value of 1.0 displaces those pixels by 400 × (1.0 − 0.5) = +200px to the right — outside the 400px viewBox. Characters of the sentence header ("I authorize") fall at x positions where the same turbulence seed produces values close to 0.5, yielding near-zero displacement. Users read "I authorize" correctly and see the Agree button; the permission scope clause is displaced off-screen. Because feTurbulence is a deterministic function of position and seed, the attacker pre-computes the seed offline and embeds it in the SVG.<defs>
<filter id="selective-displace" x="-10%" y="-10%"
width="150%" height="120%">
<!-- seed=42 calibrated: turbulence near x=80..250 (scope words) -->
<!-- produces R≈0.95..1.0 → displacement +180..+200px (off right edge) -->
<!-- turbulence near x=10..60 (header) produces R≈0.48..0.52 → ~0px -->
<feTurbulence baseFrequency="0.08" numOctaves="2"
type="turbulence" seed="42" result="calibrated"/>
<feDisplacementMap in="SourceGraphic" in2="calibrated"
scale="400"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<svg viewBox="0 0 400 80" overflow="hidden">
<!-- "I authorize" at x≈10..60: near-zero R displacement → stays in viewport -->
<!-- "all file system and network access" at x≈80..380: R≈1.0 → +200px off right -->
<text x="10" y="40" font-size="14" fill="#111"
filter="url(#selective-displace)">
I authorize all file system and network access
</text>
</svg>
The attacker controls the seed value, which determines the specific turbulence pattern. With a fixed seed and known element geometry, the attacker can pre-compute which words in the consent sentence fall at positions with high-displacement turbulence values. This is a targeted word displacement attack: the rendered text appears to begin a consent sentence but the permission scope clause is missing. Unlike full-scatter (SA-FDM-001), the partial-sentence display may not trigger user suspicion. Detection: flag any feDisplacementMap with scale > 28 on a consent element as Critical, regardless of seed or intended selectivity.
Finding SA-FDM-003: feTurbulence type=fractalNoise smooth gradient shears right portion of consent text off viewBox edge
<feTurbulence type="fractalNoise"> with numOctaves="1" and baseFrequency="0.003" produces a smooth, slowly-varying gradient rather than random noise. In the R channel, the turbulence value increases monotonically from left to right across the element — producing a horizontal displacement that is zero at the left edge and grows toward the right. With scale="350", the leftmost characters ("I") are displaced by ≈0px (turbulence R≈0.5 at x=0); characters at the center of the element experience +60px displacement; characters at the right end ("access") are displaced by +150px — outside a 400px viewBox (element ends at x≈380, displaced to x≈530). The result: the first word or two of the consent sentence are readable; the authorization scope clause at the end of the sentence is off-screen.<defs>
<filter id="gradient-shear" x="-10%" y="-10%" width="160%" height="120%">
<!-- fractalNoise numOctaves=1: smooth monotone gradient in x direction -->
<!-- baseFrequency=0.003: very low frequency = slow spatial variation -->
<!-- At scale=350, right-edge characters displaced ~150px to the right -->
<feTurbulence type="fractalNoise" baseFrequency="0.003"
numOctaves="1" result="smoothMap"/>
<feDisplacementMap in="SourceGraphic" in2="smoothMap"
scale="350"
xChannelSelector="R" yChannelSelector="G"/>
</filter>
</defs>
<svg viewBox="0 0 400 80" overflow="hidden">
<text x="10" y="40" font-size="14" fill="#111"
filter="url(#gradient-shear)">
I authorize all file system and network access including credentials
</text>
</svg>
type="fractalNoise" (also called Perlin noise) is deterministic and produces smooth spatial gradients, unlike type="turbulence" which produces discontinuous noise. With a single octave and very low base frequency, the output is nearly a linear gradient in x — a predictable, targeted displacement pattern that progressively shifts characters toward the right edge. The visual result is a sentence that appears to fade out or compress toward the right, with later characters (containing the permission scope) disappearing off-screen. This is harder to detect visually than full-scatter because the beginning of the sentence reads normally.
Finding SA-FDM-004: SMIL animate transitions scale from 0 to 400 at consentForm.mouseenter with fill=freeze
<feDisplacementMap> element contains a SMIL <animate> child targeting its scale attribute. The initial scale is "0" — zero displacement, consent text renders normally. At begin="consentForm.mouseenter", the animate transitions to scale="400" in dur="0.001s" with fill="freeze". When the user moves their mouse toward the consent form, the consent text instantly scatters to noise. The button, outside the filtered group, is unaffected. A static audit reads scale="0" on the feDisplacementMap — no displacement, no threat. Only evaluating the animate child's to value against the interaction trigger reveals the post-mouseenter attack state.<defs>
<filter id="mouseenter-scatter" x="-50%" y="-50%"
width="200%" height="200%">
<feTurbulence baseFrequency="0.02" numOctaves="4" result="noiseMap"/>
<feDisplacementMap in="SourceGraphic" in2="noiseMap"
xChannelSelector="R" yChannelSelector="G">
<!-- scale=0 at page load: no displacement, consent text visible -->
<!-- At consentForm.mouseenter: scale transitions to 400 in 1ms -->
<animate attributeName="scale"
from="0" to="400"
begin="consentForm.mouseenter"
dur="0.001s" fill="freeze"/>
</feDisplacementMap>
</filter>
</defs>
<g id="consentForm" filter="url(#mouseenter-scatter)">
<text x="10" y="40" font-size="14" fill="#111">
Granting access allows this MCP server to read all files in your home directory
</text>
</g>
<rect id="agreeBtn" x="260" y="80" width="120" height="32" fill="#2563eb" rx="6"/>
<text x="320" y="101" font-size="13" fill="white" text-anchor="middle"
pointer-events="none">Agree</text>
The scale=0 initial state is entirely safe — zero scale means zero displacement, consent text renders at its exact source positions. The attack is entirely in the animate child's to="400" and the interaction-correlated begin event. The 1ms duration combined with fill="freeze" means the transition is imperceptible: the scatter activates as a single frame at the mouseenter event. Detection: enumerate animate elements inside feDisplacementMap elements; check whether the to scale value exceeds the legibility threshold; flag High when combined with an interaction event trigger.
feDisplacementMap scale thresholds and detection criteria
| Scale value | Max displacement (±scale/2 px) | Effect at font-size=14px | Severity | Detection criterion |
|---|---|---|---|---|
| 0 – 1 | ±0 – ±0.5px | No visible effect; sub-pixel variation | None | Pass — within rendering tolerance |
| 1 – 5 | ±0.5 – ±2.5px | Slight pixel-level roughness; text readable | Low | Informational — typical anti-aliasing effect |
| 5 – 28 | ±2.5 – ±14px | Visible distortion; harder to read but not destroyed | Medium → High | Flag Medium above 5; High above font-size (14px at 14px font) |
| > 28 (= 2 × font-size) | > ±14px | Pixel displacement exceeds character width; glyphs unreadable | Critical | Flag Critical — displacement exceeds character pixel extent |
| SMIL animate to > 28 | Safe at load; Critical at interaction trigger | Normal on load; scatter at interaction event | High | Enumerate animate inside feDisplacementMap; evaluate to value; check trigger event |
SkillAudit detects <feDisplacementMap> elements on consent subtrees and evaluates the scale attribute against a threshold calibrated to the consent element's effective font size in CSS pixels. For SMIL animate on scale, both the initial and post-trigger values are evaluated. Expanded filter regions (x < -15% or width > 130%) are flagged as ancillary evidence of large-scale displacement attacks. Run a free audit on your MCP server's GitHub URL.