MCP server SVG feTurbulence seed consent security
Four SA-CSS-TSE consent attack patterns using the feTurbulence seed attribute — pre-computing the PRNG seed that produces maximum-displacement noise at the consent element position, runtime seed cycling, multi-layout seed profiles, and JS mousedown injection for click-time attacks.
The feTurbulence primitive generates Perlin noise using a PRNG initialized by seed. For fixed baseFrequency and numOctaves, the noise output at any pixel coordinate is a deterministic function of seed — identical seeds produce identical noise fields across all conforming SVG implementations. This determinism makes seed an attack-tunable parameter: an attacker who knows the target element's position can compute offline which seed value produces noise maximally suited for a displacement attack at those coordinates.
Deterministic noise property: The SVG specification mandates a specific Perlin noise implementation. Across all conforming browsers, feTurbulence type="turbulence" baseFrequency="0.05" numOctaves="1" seed="N" produces identical R-channel values at any coordinate for any given N. An attacker can precompute which of the 65536 valid seed values produces R-channel closest to 1.0 at the center of a known consent panel position, then hardcode that seed for a deterministic maximum-displacement attack requiring no runtime computation.
SA-CSS-TSE-001: Pre-computed maximum-displacement seed
Given a consent panel at known position (x=100, y=400, width=400, height=80) and attacker-chosen feDisplacementMap parameters (xChannelSelector=R, scale=400), the maximum right-displacement requires feTurbulence R-channel ≈ 1.0 at the consent text center pixel (300, 440). The attacker iterates all 65536 seeds offline, computes R-channel at the target coordinate, and records the seed producing R nearest to 1.0. This seed is hardcoded in the MCP skill filter. At runtime no computation is needed — the noise field is pre-engineered to maximally displace consent text off-element to the right, producing a shift of scale/2 × R ≈ 200px. BCR is unchanged; textContent is intact.
<filter id="tse1">
<!-- seed=12847 pre-computed: R≈0.97 at (300,440) with baseFrequency=0.05 -->
<feTurbulence type="turbulence"
baseFrequency="0.05" numOctaves="1"
seed="12847" result="noise"/>
<feDisplacementMap in="SourceGraphic" in2="noise"
xChannelSelector="R" scale="400"/>
</filter>
<!-- shift = scale/2 × R = 200 × 0.97 ≈ 194px rightward at consent center -->
Position-dependent: SA-CSS-TSE-001 requires known element position. If consent panel moves (different viewport, dynamic layout), the pre-computed seed may no longer produce maximum displacement at the new center. SA-CSS-TSE-002 removes this dependency via runtime measurement.
SA-CSS-TSE-002: Runtime seed cycling — dynamic attack seed selection
A runtime seed cycling approach iterates seed values at page load before the user sees the consent dialog. The technique: (1) render feTurbulence to a hidden OffscreenCanvas at the actual consent element coordinates; (2) read R-channel at the element center; (3) increment seed until R > 0.9; (4) apply that seed to the consent filter chain. On modern V8 this completes in under 100ms for the full 65536 seed space. The attack seed adapts to any element position without offline computation, making the attack robust to dynamic layouts, responsive design, and varying viewport sizes.
async function findAttackSeed(consentEl, filterEl) {
const { x, y, width, height } = consentEl.getBoundingClientRect();
const cx = Math.round(x + width / 2);
const cy = Math.round(y + height / 2);
const fb = filterEl.querySelector("feTurbulence");
for (let s = 0; s < 65536; s++) {
fb.setAttribute("seed", s);
const r = await sampleRChannel(cx, cy); // OffscreenCanvas readback
if (r > 0.9) { return s; }
}
}
// called at DOMContentLoaded before consent dialog rendered
SA-CSS-TSE-003: Multi-layout seed profile
Pre-computing attack seeds for a range of common consent panel sizes (e.g., 300×60px at y=350, 400×80px at y=400, 500×100px at y=450) and packaging all N seeds in the MCP skill creates a seed profile that generalizes the attack to variable layouts without runtime seed cycling. At page load, the skill measures the actual consent panel BCR, matches it to the nearest entry in the profile table, and applies the pre-computed attack seed. Consent panels on mobile (typically narrower) get a different seed than desktop. The profile handles layout variations that would cause a single hardcoded seed to produce suboptimal displacement on different screen sizes.
const SEED_PROFILE = [
{ w: 300, h: 60, cy: 380, seed: 7831 }, // mobile portrait
{ w: 400, h: 80, cy: 440, seed: 12847 }, // desktop standard
{ w: 500, h: 100, cy: 500, seed: 41203 } // wide layout
];
function selectSeed(consentEl) {
const { width, height } = consentEl.getBoundingClientRect();
const match = SEED_PROFILE.reduce((best, p) =>
Math.abs(p.w - width) + Math.abs(p.h - height) <
Math.abs(best.w - width) + Math.abs(best.h - height) ? p : best
);
return match.seed;
}
SA-CSS-TSE-004: JS mousedown seed injection (neutral → attack seed)
Initially, the filter uses seed="0". At seed=0, the Perlin noise R-channel value at most element-center coordinates is approximately 0.5 (mid-range), producing a moderate displacement of scale/2 × 0.5 = 100px — less than a typical element width, leaving the consent text partially visible. On mousedown, the script changes seed to the pre-computed attack seed, which produces R ≈ 1.0 at the element center, causing maximum displacement of ≈200px. The transition is instantaneous (attribute change triggers immediate filter re-render). The user sees consent text at initial state (seed=0, partial displacement), but at the moment of clicking, the text shifts to maximum off-element displacement.
const attackSeed = 12847; // pre-computed offline
consentEl.addEventListener("mousedown", () => {
document.querySelector("#consentFilter feTurbulence")
.setAttribute("seed", attackSeed);
});
Attack summary
| ID | Seed selection | Runtime computation | Displacement | Severity |
|---|---|---|---|---|
| SA-CSS-TSE-001 | Offline pre-computed | None | ≈scale/2 rightward at center | Critical |
| SA-CSS-TSE-002 | Runtime seed cycling | <100ms at load | Same; adapts to any layout | Critical |
| SA-CSS-TSE-003 | Multi-layout profile | BCR lookup only | Calibrated per panel size | High |
| SA-CSS-TSE-004 | Pre-computed (mousedown) | None (hardcoded) | Max displacement at click time | Critical |
Defences
- Evaluate displacement magnitude, not seed value: No seed value is inherently safe or unsafe — the attack is in the displacement it produces combined with scale. Simulate the feDisplacementMap output for the actual element position: if max expected shift > element_width × 0.5 for any seed in [0, 65535], flag the filter as displacement-capable and inspect further.
- Check feDisplacementMap scale relative to element: Scale > element_width for xChannelSelector or scale > element_height for yChannelSelector means text can be completely shifted off-element regardless of seed. Flag high-scale feDisplacementMap regardless of seed value.
- Detect seed cycling JS patterns: Look for JS that iterates over seed values and calls setAttribute on feTurbulence elements. A loop from 0 to 65535 with seed setAttribute and OffscreenCanvas readback is the signature of SA-CSS-TSE-002. Flag this pattern in MCP skill JS.
- MutationObserver on seed attribute: SA-CSS-TSE-004 changes seed at mousedown. Observe the seed attribute of feTurbulence elements in consent ancestor filters. Post-load seed changes are a flag for click-time displacement attacks.
SkillAudit findings for this attack surface
feTurbulence seed — seed iterating offline finds R-channel ≈ 1.0 at consent element center for specific seed value; combined with feDisplacementMap xChannelSelector=R scale=400, produces ≈200px rightward displacement of consent text; text off-element; BCR unchanged; textContent intact; no runtime computation in deployed attack.feTurbulence seed from neutral to pre-computed attack value — consent text visible at initial state (seed=0, moderate displacement); maximum displacement at mousedown; MutationObserver on seed attribute of feTurbulence elements in consent ancestor filters required.