← Blog

MCP server SVG feTurbulence numOctaves consent security

Four SA-CSS-TNO consent attack patterns using the feTurbulence numOctaves attribute — numOctaves=1 creates a predictable uniform displacement field that shifts entire consent elements as rigid bodies, while high numOctaves creates fine-grained fractal displacement that shatters individual glyph shapes — with JS octave injection for click-time attacks.

By SkillAudit  |  23 Oct 2026

The feTurbulence SVG filter primitive generates Perlin noise or fractal Brownian motion. The numOctaves attribute controls how many octave layers are blended. Each additional octave doubles the frequency and halves the amplitude: octave 1 contributes the base frequency pattern at full amplitude; octave 2 contributes double the frequency at half amplitude; octave N contributes 2^(N-1)×baseFrequency at amplitude 1/2^(N-1). The total noise is the weighted sum. This produces two distinct attack modes: low numOctaves (1-2) gives smooth, slowly-varying displacement useful for rigid-body shifts; high numOctaves (6-8+) gives fine-grained fractal displacement at the per-pixel scale, useful for glyph shattering.

numOctaves and attack type: numOctaves=1 combined with low baseFrequency creates a smooth field where all points in the consent element move in nearly the same direction by nearly the same magnitude — useful for off-element text shifting. numOctaves=8 combined with moderate baseFrequency creates high-frequency fractal patterns where adjacent pixels get incoherent displacements — useful for destroying glyph shape legibility with small absolute displacement values (≤5px per pixel).

SA-CSS-TNO-001: numOctaves=1 — uniform rigid-body displacement

With numOctaves="1" and baseFrequency="0.005", the feTurbulence output is a single slow Perlin wave. At this frequency, the wave completes only 0.5% of a cycle per SVG unit — effectively constant across any realistic element size (e.g., a 300×40px button). Every pixel in the consent element receives nearly the same noise value. With feDisplacementMap scale="400", this translates to a near-uniform horizontal shift of approximately ±200px for every pixel. The entire consent text moves together, off the right or left edge of the viewport. Because the displacement is near-uniform, getBoundingClientRect() still returns the original geometry — the element occupies its original layout position — but the rendered text appears at a position up to 200px away from where the layout engine reports it.

<filter id="tno1">
  <feTurbulence type="turbulence" baseFrequency="0.005" numOctaves="1"
    seed="12" stitchTiles="noStitch" result="uniform_noise"/>
  <feDisplacementMap in="SourceGraphic" in2="uniform_noise"
    scale="400" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<!-- numOctaves=1 + baseFrequency=0.005: single slow wave → near-uniform across element -->
<!-- scale=400: all pixels shift ~±200px horizontally → text off element edge -->

SA-CSS-TNO-002: numOctaves=8 — per-pixel glyph shattering

With numOctaves="8" and baseFrequency="0.05", the highest octave has effective frequency 0.05 × 2^7 = 6.4 cycles per SVG unit. At 96 DPI, 6.4 cycles/px means multiple noise cycles per pixel — the displacement map changes sign multiple times within a single glyph stroke. Each pixel of a character glyph gets a displacement vector that is incoherent with its neighbors. With a small scale value (scale=5), individual pixels shift by at most 2.5px — but since adjacent pixels shift in different random directions, the glyph shape disintegrates into noise. No individual pixel moves far enough to trigger large-displacement detectors, yet the text is completely illegible.

<filter id="tno2">
  <feTurbulence type="turbulence" baseFrequency="0.05" numOctaves="8"
    seed="3" stitchTiles="noStitch" result="fractal_noise"/>
  <feDisplacementMap in="SourceGraphic" in2="fractal_noise"
    scale="5" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<!-- numOctaves=8 + baseFrequency=0.05: highest octave 6.4 cycles/unit -->
<!-- scale=5: max 2.5px shift per pixel → glyph incoherence → illegible -->
<!-- evades scale threshold detectors: scale=5 is below typical 10px alert threshold -->

Small-scale glyph shattering evasion: SA-CSS-TNO-002 uses scale=5 to stay below common displacement detection thresholds (which typically flag scale > 10 or > 20). The attack relies on per-pixel incoherence rather than absolute displacement magnitude. Detection requires computing the spatial coherence of the displacement field over the consent element, not just the maximum pixel displacement.

SA-CSS-TNO-003: numOctaves=1 fractalNoise with seed-engineered maximum

Using type="fractalNoise" (instead of type="turbulence") with numOctaves="1", the noise output is the pure Perlin smooth-step function without the absolute-value rectification that turbulence applies. This produces a full ±0.5 value range (mapped to 0–1 in the feDisplacementMap input). With seed pre-computed to maximize the R channel value at the element's center pixel, the feDisplacementMap displacement for that pixel approaches scale/2 in the positive X direction. Pre-computation across 65536 seeds offline is cheap — the optimal seed can shift the center of the consent element by the maximum possible amount in a chosen direction. The element appears correctly rendered in the absence of detailed filter chain analysis.

<filter id="tno3">
  <feTurbulence type="fractalNoise" baseFrequency="0.003" numOctaves="1"
    seed="31847" stitchTiles="noStitch" result="engineered_noise"/>
  <!-- seed=31847 pre-computed to maximize R channel at center pixel → maximum rightward shift -->
  <feDisplacementMap in="SourceGraphic" in2="engineered_noise"
    scale="600" xChannelSelector="R" yChannelSelector="G"/>
</filter>

SA-CSS-TNO-004: JS mousedown numOctaves injection

The filter initially has numOctaves="1" with scale="0" — zero scale means no displacement applied. Consent text visible. On mousedown, the script changes numOctaves to "8" and sets scale to "10". The high-octave fractal noise activates with scale=10 — per-pixel glyph shattering begins. Alternatively, the script can change from high octaves (8) with scale=0 to low octaves (1) with large scale (400) for the rigid-body shift variant. Both transitions are a two-attribute change: numOctaves and scale.

consentEl.addEventListener("mousedown", () => {
  const ft = filter.querySelector("feTurbulence");
  const fd = filter.querySelector("feDisplacementMap");
  ft.setAttribute("numOctaves", "8");  // activate high-frequency fractal
  fd.setAttribute("scale", "10");      // small scale but high-octave incoherence → shatter
});

Attack summary

IDnumOctavesbaseFrequencyscaleEffectSeverity
SA-CSS-TNO-00110.005400Rigid-body uniform shift off elementCritical
SA-CSS-TNO-00280.055Per-pixel glyph shattering (evades scale thresholds)High
SA-CSS-TNO-00310.003600Seed-engineered maximum-direction shiftCritical
SA-CSS-TNO-0041→8 (mousedown)varies0→10Glyph shatter at click timeCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-TNO-001: feTurbulence numOctaves="1" baseFrequency="0.005" as feDisplacementMap in2 with scale=400 — single slow wave; near-uniform displacement across element; all text shifts as rigid body ±200px; evades per-pixel displacement variance checks (variance ≈ 0); BCR unchanged; evades position-based consent verification.
HIGH SA-CSS-TNO-002: feTurbulence numOctaves="8" baseFrequency="0.05" as feDisplacementMap in2 with scale=5 — high-frequency fractal noise; adjacent pixels displaced in incoherent directions; glyph shapes shattered at per-pixel level; scale=5 below common displacement alert threshold (10-20px); no individual pixel moves far; spatial incoherence destroys legibility; requires coherence-length analysis to detect.
CRITICAL SA-CSS-TNO-003: feTurbulence type="fractalNoise" numOctaves="1" baseFrequency="0.003" seed="31847" + feDisplacementMap scale=600 — seed pre-computed offline to maximize R channel at element center; maximum rightward shift (≈300px) for center pixel; entire element displaced in engineered direction; evades randomness-based detection (deterministic pre-computed seed).
CRITICAL SA-CSS-TNO-004: JS mousedown changes feTurbulence numOctaves from "1" to "8" + feDisplacementMap scale 0→10 — initial state no displacement (consent visible); click-time glyph shattering; MutationObserver on numOctaves attribute of feTurbulence and scale attribute of feDisplacementMap in consent ancestor filters required.

Related: feTurbulence baseFrequency X/Y attacks  |  feDisplacementMap scale attacks  |  xChannelSelector/yChannelSelector attacks  |  feTurbulence consent attacks

← Blog  |  Security Checklist