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.
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
| ID | baseFrequency | scale | Noise pattern | Effect | Severity |
|---|---|---|---|---|---|
| SA-CSS-TBF-001 | 0 100 | 200 | Horizontal bands (constant per row) | Row-level horizontal scramble | Critical |
| SA-CSS-TBF-002 | 100 0 | 8 | Vertical stripes (constant per column) | Glyph vertical fragmentation | High |
| SA-CSS-TBF-003 | 0.01 0 | 500 | Near-uniform macro-field | Entire element macro-shift off-element | Critical |
| SA-CSS-TBF-004 | 0 0 → 0 100 (mousedown) | 0→200 | Identity → horizontal bands | Row scramble at click time | Critical |
Defences
- Check for anisotropic baseFrequency: Parse the two-component form of baseFrequency. If either component is zero and the other is nonzero, flag for anisotropic noise analysis. Do not rely solely on isotropic noise models to evaluate feDisplacementMap attack potential.
- Simulate row-level displacement for horizontal-band noise: For baseFrequency="0 N", compute the average noise value per row by sampling feTurbulence output at representative Y positions. If the row-level displacement variance (in pixels) exceeds 1 character width, flag high-severity row-scramble attack.
- Low-frequency macro-shift detection: For baseFrequency with max component < 0.05, the noise field is near-uniform. Treat as feDisplacementMap with a constant displacement field and flag if scale × average_noise_magnitude > element bounding box width/2.
- MutationObserver on feTurbulence baseFrequency and feDisplacementMap scale: SA-CSS-TBF-004 changes both. Observe the
baseFrequencyattribute on feTurbulence elements and thescaleattribute on feDisplacementMap elements in consent ancestor filters.
SkillAudit findings for this attack surface
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.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.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.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.