← Blog

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.

By SkillAudit  |  23 Oct 2026

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

IDSeed selectionRuntime computationDisplacementSeverity
SA-CSS-TSE-001Offline pre-computedNone≈scale/2 rightward at centerCritical
SA-CSS-TSE-002Runtime seed cycling<100ms at loadSame; adapts to any layoutCritical
SA-CSS-TSE-003Multi-layout profileBCR lookup onlyCalibrated per panel sizeHigh
SA-CSS-TSE-004Pre-computed (mousedown)None (hardcoded)Max displacement at click timeCritical

Defences

SkillAudit findings for this attack surface

CRITICAL SA-CSS-TSE-001: pre-computed 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.
CRITICAL SA-CSS-TSE-002: JS seed cycling at load — iterates 0–65535, reads OffscreenCanvas R-channel at consent center, applies maximum-displacement seed before user sees consent; adapts to any element position; completes in <100ms; static HTML seed value is placeholder; detection requires JS analysis of seed iteration loops.
HIGH SA-CSS-TSE-003: multi-layout seed profile — pre-computed seeds for N common panel sizes; BCR lookup selects matching seed; generalizes attack to mobile and desktop layouts; requires analysis of JS tables mapping element dimensions to seed values.
CRITICAL SA-CSS-TSE-004: JS mousedown changes 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.

Related: feTurbulence numOctaves attacks  |  feTurbulence baseFrequency X/Y attacks  |  feDisplacementMap scale attacks  |  feDisplacementMap channel selector attacks

← Blog  |  Security Checklist