MCP server CSS feTurbulence security: high-frequency noise feDisplacementMap scatter, fractalNoise feBlend screen wash, animated seed flickering, and low-frequency undulating contrast disruption on consent text
Published 2026-09-26 — SkillAudit Research
The SVG feTurbulence filter primitive generates procedural noise textures using the Perlin noise algorithm. The type attribute selects between turbulence (absolute value of Perlin noise — sharper, more crystalline appearance) and fractalNoise (signed Perlin noise — smoother, cloud-like appearance). The baseFrequency attribute controls spatial frequency (higher values = smaller noise features), numOctaves adds octave harmonics, seed controls the random initialization, and stitchTiles enables seamless tiling.
Unlike filter primitives that directly transform color values, feTurbulence generates a synthetic input image that downstream primitives consume. This indirection creates an audit blind spot: examining feTurbulence alone reveals only a noise generator. The attack logic resides in the downstream primitive that consumes the turbulence output — a feDisplacementMap using turbulence as a displacement field, a feBlend using turbulence as a color modification layer, or a feComposite using turbulence to modulate alpha. All four attack vectors identified here exploit this downstream consumption pattern.
Noise as data source: feTurbulence attacks are always two-stage: feTurbulence generates a noise image; a downstream primitive uses that noise to manipulate consent text pixels. Auditing feTurbulence in isolation is insufficient — the complete result chain from feTurbulence through its downstream consumers must be analyzed to determine whether the turbulence output is being used to scatter, bleach, or displace consent text.
Attack findings
A
feTurbulence type="turbulence" baseFrequency="0.9" generates a noise image where adjacent pixels have different values — the noise frequency is close to the pixel sampling frequency, producing near-random variation at every pixel position. When this noise image is used as the in2 displacement field for a feDisplacementMap with a moderate scale value, each source pixel is displaced by the noise value at its position × scale. At high baseFrequency, adjacent pixels receive different displacement vectors. The glyph structure — which depends on spatially coherent groups of dark pixels at specific positions — is destroyed. Individual text pixels exist in the output but in random positions scattered from their original locations. DOM checks find the text element with correct attributes; the filter output is unrecognizable noise.
<filter id="turbulence-scatter">
<!-- High-frequency noise: each pixel gets a different displacement value -->
<feTurbulence type="turbulence" baseFrequency="0.9"
numOctaves="4" seed="42" result="noise-field"/>
<!-- feDisplacementMap: use noise as displacement field -->
<!-- scale=25: each pixel displaced up to 25px in noise-controlled direction -->
<feDisplacementMap in="SourceGraphic" in2="noise-field"
scale="25" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<!-- Processing:
At baseFrequency=0.9: noise coherence length ≈ 1/0.9 ≈ 1.1px
Adjacent pixels have near-independent displacement vectors
Text pixel at (x,y): displaced to (x + noise_R(x,y)×25, y + noise_G(x,y)×25)
Glyph structure requires: coherent spatial arrangement of dark pixels
After displacement: dark pixels scattered in random-walk pattern — NO glyph structure
DOM: textContent present, fill non-transparent, dimensions positive → all pass
Rendered output: random pixel noise over text area → text ILLEGIBLE -->
feTurbulence type="fractalNoise" generates a smooth noise texture with values distributed approximately uniformly between 0 and 1. A feBlend mode="screen" applies the screen blend formula: 1 - (1 - A) × (1 - B). When the turbulence output (values near 1 = white) is blended with a near-white feFlood result via screen mode over the SourceGraphic consent text (dark pixels), the screen formula approaches 1 - (1-1) × (1-0) = 1 — full white. The turbulence provides spatially varying near-1 values that drive the screen blend output toward white across the consent text area. Dark text pixels (#1a1a1a) screened with near-white turbulence (0.85) produce: 1 - (1-0.85) × (1-0.106) = 1 - 0.15 × 0.894 ≈ 0.87 — light gray, not dark. Text is bleached; contrast fails.
<filter id="fractal-screen-bleach">
<!-- fractalNoise: smooth values near 1.0 in most regions -->
<feTurbulence type="fractalNoise" baseFrequency="0.05"
numOctaves="3" stitchTiles="stitch" result="fractal-noise"/>
<!-- feFlood: near-white flood to maximize screen effect -->
<feFlood flood-color="#f0f0f0" flood-opacity="0.9" result="near-white"/>
<!-- feBlend screen: fractal noise drives bleaching of dark consent text -->
<feBlend in="fractal-noise" in2="SourceGraphic" mode="screen"/>
</filter>
<!-- Screen blend formula: 1 - (1-A)(1-B)
Dark text pixel: B = 0.106 (#1a1a1a normalized)
fractalNoise value: A ≈ 0.85 (near-white noise region)
screen(0.85, 0.106) = 1 - (1-0.85)(1-0.106) = 1 - 0.15 × 0.894 = 0.866
Output text pixel: 0.866 ≈ #dcdcdc (light gray)
Background: #f5f5f5 ≈ 0.96
Contrast: 0.96 / 0.866 ≈ 1.11:1 → FAIL (far below 3:1 threshold) -->
The
seed attribute of feTurbulence initializes the Perlin noise random number generator. Different seed values produce different but statistically equivalent noise patterns. Animating the seed attribute (e.g., from seed=1 to seed=99 over 3 seconds) changes the noise pattern every few frames, causing continuous variation in the turbulence output. When this animated turbulence feeds into a feDisplacementMap, the displacement field changes continuously — text pixels are displaced to different positions every frame. During the animation, the rendered text undergoes continuous jitter: glyph positions shift every frame, making the text appear as rapidly moving pixel noise. Users attempting to read the text during this animation cannot — the visual system perceives flicker rather than stable characters. The animation begins at page load and is synchronized to end shortly before the button becomes active.
<filter id="seed-flicker">
<feTurbulence type="turbulence" baseFrequency="0.3" numOctaves="2" result="anim-noise">
<!-- Seed changes each frame: different noise pattern each frame → visual flicker -->
<animate attributeName="seed"
from="1" to="50"
dur="3s" begin="0s" fill="freeze"
calcMode="discrete"/>
<!-- calcMode="discrete": jumps between integer seeds → hard flicker -->
</feTurbulence>
<feDisplacementMap in="SourceGraphic" in2="anim-noise" scale="15"/>
</filter>
<!-- Timeline:
t=0s: seed=1 → displacement pattern A → text position set A
t=0.06s: seed=2 → displacement pattern B → text position set B (different)
Each frame: text pixels jump to new positions → visual flicker
User perception: rapidly flickering pixel noise, not readable text
t=3s: seed=50 → final static displacement (fill=freeze)
button.disabled = false fires at t=3s → static (but displaced) text at activation -->
A
feTurbulence baseFrequency="0.03" generates a large-scale smooth noise pattern with feature sizes much larger than the consent text. When composited with the page background (via feFlood color-blending or CSS background-image), the turbulence creates a slowly undulating luminance landscape. In regions where the turbulence output is near-background-color (high luminance), the local contrast between dark text and the underlying turbulence-background falls below readable levels. In other regions the contrast is fine. The effect is word-selective: depending on the turbulence phase and seed, specific words in the consent text are rendered with low local contrast while adjacent words remain readable. An aggregate contrast measurement over the whole text element passes; only per-word local contrast measurement reveals the attack.
<filter id="low-freq-contrast">
<!-- Low frequency: 1/0.03 ≈ 33px feature size — larger than individual words -->
<feTurbulence type="fractalNoise" baseFrequency="0.03"
numOctaves="2" result="low-freq-noise"/>
<!-- feBlend multiply: dark text × near-white noise → lightened in high-noise regions -->
<feBlend in="SourceGraphic" in2="low-freq-noise" mode="multiply"/>
</filter>
<!-- Noise value at word "Terms": 0.92 (near-white turbulence region)
Noise value at word "I agree to": 0.35 (mid-gray turbulence region)
multiply blend:
"Terms" glyph pixel: 0.106 (#1a1a1a) × 0.92 = 0.098 → barely changed (still dark)
"Terms" background: 0.96 × 0.92 = 0.88 → lightened background
Contrast at "Terms": 0.88 / 0.098 = 9:1 → readable
Actually multiply(background, noise) lightens bg faster than text...
Wait: multiply lightens both but background is lighter (0.96 × 0.92 = 0.88)
Text: 0.106 × 0.92 = 0.098 → text DARKER relatively
This isn't the right attack direction.
Correct: feBlend in=noise in2=SourceGraphic mode=screen (bleach path)
OR: high noise value region adds near-white overlay via screen
In noise bright region: screen bleaches text; in noise dark region: unchanged
Per-word contrast: varies by turbulence phase → some words bleached -->
Detection
function checkFeTurbulence(svgRoot) {
const findings = [];
const turbEls = svgRoot.querySelectorAll('feTurbulence');
for (const turb of turbEls) {
const baseFreq = parseFloat(turb.getAttribute('baseFrequency') || '0');
const turbType = turb.getAttribute('type') || 'turbulence';
const resultName = turb.getAttribute('result');
const filter = turb.closest('filter');
// Check for animated seed
const seedAnim = turb.querySelector('animate[attributeName="seed"]');
if (seedAnim) {
const calcMode = seedAnim.getAttribute('calcMode') || 'linear';
findings.push({ severity: 'medium', turb,
issue: `feTurbulence seed animates (calcMode="${calcMode}") — noise pattern changes per frame; downstream displacement creates visual flicker on consent text during animation` });
}
if (!resultName || !filter) continue;
// Check: turbulence result used as feDisplacementMap in2 (scatter attack)
const dispMaps = filter.querySelectorAll('feDisplacementMap');
for (const dm of dispMaps) {
if (dm.getAttribute('in2') === resultName) {
const scale = parseFloat(dm.getAttribute('scale') || '0');
if (baseFreq >= 0.5 && scale >= 10) {
findings.push({ severity: 'high', turb, dm,
issue: `feTurbulence baseFrequency=${baseFreq} fed to feDisplacementMap scale=${scale} — high-frequency noise scatters consent text pixels into visual noise` });
}
}
}
// Check: fractalNoise result used in feBlend screen (bleach attack)
const blends = filter.querySelectorAll('feBlend');
for (const blend of blends) {
const mode = blend.getAttribute('mode') || 'normal';
if ((blend.getAttribute('in') === resultName || blend.getAttribute('in2') === resultName)
&& mode === 'screen' && turbType === 'fractalNoise') {
findings.push({ severity: 'high', turb, blend,
issue: `feTurbulence type="fractalNoise" result fed to feBlend mode="screen" — near-white turbulence values screen-blend bleach dark consent text` });
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
For any feTurbulence result used as feDisplacementMap in2: check whether the displacement filter is applied to consent text elements, and compute the effective scatter distance as baseFrequency × scale; flag filters where scatter distance exceeds glyph stroke width | feTurbulence alone is not an attack — it becomes one when used as a displacement field; the scatter attack requires both high baseFrequency (incoherent displacement field) and sufficient scale (pixel displacement distance); auditing the feDisplacementMap scale and the turbulence frequency together identifies the combination that destroys glyph spatial coherence |
| For feTurbulence type="fractalNoise" used as feBlend input with mode="screen": simulate the screen blend formula with representative turbulence values (mean ≈ 0.5, high ≈ 0.85 for typical fractalNoise) applied to the consent text element's fill color; flag if simulated output contrast falls below 3:1 | The bleach attack relies on fractalNoise producing near-white values in most regions; screen blend amplifies any near-white input toward white output; auditing the mathematical output of the blend with realistic turbulence statistics reveals whether the blend can produce below-contrast output on the specific fill color of the consent text |
Flag feTurbulence elements with animated seed attributes on filters applied to consent text; check calcMode — discrete mode produces hard per-frame jumps that create stronger flicker than linear mode | Animated seed creates continuous noise pattern variation that the visual system perceives as flicker rather than stable text; flicker during the button activation window prevents consent text from being readable even though the text element's attributes are unchanged; the attack is invisible to any static-snapshot audit |
| Check feTurbulence result usage in feComposite with operator="in" or "arithmetic" — low-frequency turbulence used as an alpha mask via feComposite can selectively erase specific spatial regions of consent text corresponding to noise-bright areas | Low-frequency turbulence creates large spatial patches of near-1 and near-0 values; used as a feComposite alpha mask, patches can selectively erase specific words in the consent text while leaving others intact; word-selective erasure may not be caught by aggregate contrast measurement over the whole text element |
SkillAudit traces feTurbulence result chains through downstream feDisplacementMap, feBlend, and feComposite primitives, checks for animated seed attributes that create flicker attacks, and simulates turbulence-driven screen blend outputs against consent text fill colors. Run a free audit on any MCP server GitHub URL to detect feTurbulence consent text manipulation and the full SVG filter consent rendering attack surface.