← Blog

MCP server SVG feTurbulence baseFrequency X/Y consent security

Four SA-CSS-TBF consent attack patterns using the separate X and Y components of the feTurbulence baseFrequency attribute — zero X / high Y for horizontal band displacement, high X / zero Y for vertical column fragmentation, very low X for row-level uniform shift — with JS frequency injection for click-time attacks.

By SkillAudit  |  23 Oct 2026

The feTurbulence SVG filter primitive generates procedural noise. When used as the in2 displacement map for feDisplacementMap, the noise values control per-pixel displacement of the source graphic. The baseFrequency attribute accepts a single value (isotropic noise) or two values "bx by" that set independent X and Y frequencies. This separation allows attackers to construct anisotropic noise patterns — noise that varies only along one axis — creating structured horizontal band or vertical stripe displacement that cannot be detected by isotropic noise analysis alone.

Anisotropic noise property: When baseFrequency="0 N" (X=0, Y=N), the feTurbulence output is constant along every horizontal scanline — every pixel in the same row gets the same noise value. This turns feDisplacementMap into a row-level uniform shift: each text row is shifted horizontally by a different amount. With a moderate scale (≥ 2× character width), all rows are shifted out of their expected positions, making the text unreadable even though individual glyph shapes are preserved.

SA-CSS-TBF-001: baseFrequency="0 100" — horizontal band displacement

Setting baseFrequency="0 100" (bx=0, by=100) suppresses all horizontal variation in the noise field. Every pixel within the same horizontal scanline receives the same turbulence value. When this is used as in2 for feDisplacementMap with xChannelSelector="R", each text row is displaced horizontally by a single consistent amount — but different rows are displaced by different amounts because the Y frequency is high (100 cycles/unit). The displacement rotates through the full noise range rapidly in the Y direction. This causes adjacent text rows to shift horizontally by different magnitudes, destroying line-by-line readability while preserving individual glyph pixel integrity. Element bounding box, textContent, and getBoundingClientRect() remain unchanged.

<filter id="tbf1">
  <feTurbulence type="turbulence" baseFrequency="0 100" numOctaves="1"
    seed="42" stitchTiles="noStitch" result="aniso_noise"/>
  <feDisplacementMap in="SourceGraphic" in2="aniso_noise"
    scale="200" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<!-- baseFrequency 0 in X: same value per row; 100 in Y: different value per row -->
<!-- each row shifts horizontally by different amount → scrambled layout -->

SA-CSS-TBF-002: baseFrequency="100 0" — vertical column fragmentation

Mirror of TBF-001: baseFrequency="100 0" (bx=100, by=0) suppresses all vertical variation. Every pixel in the same vertical column gets the same noise value. Using yChannelSelector="R", each glyph column is displaced vertically by a consistent amount — but adjacent columns are displaced differently. At high X frequency (100 cycles/unit), adjacent pixel columns cycle through the full displacement range. Each 1-pixel-wide vertical slice of a glyph is shifted by a different amount in the Y direction. The glyph shape shatters into a vertical "smear" where the column-by-column Y displacements are incoherent relative to the intended letter form. Importantly, this evades displacement detectors that only measure maximum absolute displacement magnitude — each individual column's displacement is bounded, but the vertical incoherence across columns makes individual glyphs unrecognizable.

<filter id="tbf2">
  <feTurbulence type="turbulence" baseFrequency="100 0" numOctaves="1"
    seed="1" stitchTiles="noStitch" result="vert_noise"/>
  <feDisplacementMap in="SourceGraphic" in2="vert_noise"
    scale="8" xChannelSelector="R" yChannelSelector="R"/>
</filter>
<!-- baseFrequency 100 in X: different value per column; 0 in Y: same value per column -->
<!-- each column shifts by different Y amount → glyphs shattered vertically -->
<!-- scale=8 is small enough to evade large-scale detectors but incoherence destroys readability -->

SA-CSS-TBF-003: baseFrequency="0.01 0" — row-level uniform macro-shift

Very low X frequency with zero Y frequency (baseFrequency="0.01 0") produces a slowly-varying horizontal noise field. The noise completes only 0.01 cycles per SVG unit, meaning the value changes very slowly across columns. With zero Y frequency, all rows get the same noise value (since no vertical variation). This creates a single near-uniform displacement for the entire element. Combined with a large scale (e.g., 500), the entire consent text shifts in one direction by a large amount, moving off-element or outside the filter region. The attack resembles feDisplacementMap with a solid feFlood in2, but uses feTurbulence — making it harder to detect by checking for feFlood primitives specifically. Different seed values move the text in different directions, and the optimal seed for maximum displacement can be pre-computed offline.

<filter id="tbf3">
  <feTurbulence type="turbulence" baseFrequency="0.01 0" numOctaves="1"
    seed="7" stitchTiles="noStitch" result="macro_noise"/>
  <feDisplacementMap in="SourceGraphic" in2="macro_noise"
    scale="500" xChannelSelector="R" yChannelSelector="G"/>
</filter>
<!-- low X, zero Y: near-uniform displacement field → entire element shifted by ~±250px -->
<!-- seed=7 pre-computed to maximize rightward shift → text moved off element right edge -->

Seed pre-computation: For any given baseFrequency and element size, an attacker can enumerate 65536 integer seed values offline to find the seed that maximizes displacement in the desired direction. This offline pre-computation is cheap and produces a "targeted" attack that passes any per-run randomness check.

SA-CSS-TBF-004: JS mousedown baseFrequency injection

The filter initially has baseFrequency="0 0" — when both X and Y frequency are zero, feTurbulence output is implementation-defined but typically a constant value (often 0.5 in all channels). A displacement map of 0.5 combined with scale=0 is an identity — no displacement. On mousedown, the script changes baseFrequency to "0 100" and simultaneously sets scale to 200. The anisotropic high-Y-frequency noise activates and applies row-scrambling displacement at click time. The consent text is visible before the click and disappears (scrambled) during the mousedown event.

consentEl.addEventListener("mousedown", () => {
  const ft = filter.querySelector("feTurbulence");
  const fd = filter.querySelector("feDisplacementMap");
  ft.setAttribute("baseFrequency", "0 100");
  fd.setAttribute("scale", "200");
});

Attack summary

IDbaseFrequencyscaleNoise patternEffectSeverity
SA-CSS-TBF-0010 100200Horizontal bands (constant per row)Row-level horizontal scrambleCritical
SA-CSS-TBF-002100 08Vertical stripes (constant per column)Glyph vertical fragmentationHigh
SA-CSS-TBF-0030.01 0500Near-uniform macro-fieldEntire element macro-shift off-elementCritical
SA-CSS-TBF-0040 0 → 0 100 (mousedown)0→200Identity → horizontal bandsRow scramble at click timeCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-TBF-001: feTurbulence baseFrequency="0 100" as feDisplacementMap in2 with scale=200 — horizontal-band noise (constant per row, high variation per Y position); each text row shifts horizontally by different amount; line-by-line readability destroyed; getBoundingClientRect unchanged; evades position-based checks.
HIGH SA-CSS-TBF-002: feTurbulence baseFrequency="100 0" as feDisplacementMap in2 with scale=8 — vertical-stripe noise (constant per column, high variation per X position); each glyph column shifts vertically by different amount; individual glyphs shattered; small scale (8) evades absolute-magnitude displacement detectors; glyph incoherence destroys readability.
CRITICAL SA-CSS-TBF-003: feTurbulence baseFrequency="0.01 0" as feDisplacementMap in2 with scale=500 — near-uniform noise field (slowly varying, effectively constant); seed pre-computed to maximize directional shift; entire element shifted ±250px; evades feFlood-specific in2 detectors; BCR unchanged.
CRITICAL SA-CSS-TBF-004: JS mousedown changes feTurbulence baseFrequency from "0 0" to "0 100" + feDisplacementMap scale 0→200 — initial state identity (no displacement); click-time row scramble; MutationObserver on baseFrequency of feTurbulence and scale of feDisplacementMap in consent ancestor filters required.

Related: feDisplacementMap scale attacks  |  xChannelSelector/yChannelSelector attacks  |  feTurbulence consent attacks  |  feComposite operator attacks

← Blog  |  Security Checklist