MCP Server Security · Scroll-Driven Animations · animation-timeline: scroll() · ViewTimeline · CSS Scroll Oracle · @scroll-timeline · @property

MCP server Scroll-Driven Animations security

Scroll-Driven Animations (animation-timeline: scroll(), Chrome 115+) drive CSS animations with scroll position as the timeline — no JavaScript event listener required. MCP tool output can read back the animation progress value as a scroll position oracle, use ViewTimeline to detect when specific elements become visible, and extract scroll state via CSS custom property animation — all invisible to CSP and without scroll event listener detection.

Scroll-Driven Animations API surface

/* Scroll-Driven Animations — Chrome 115+, Edge 115+, Firefox 110+ */
/* No permission required. No Permissions-Policy directive. */

/* 1. Link animation to document scroll position */
@keyframes track-scroll {
  from { --scroll-pct: 0; }
  to   { --scroll-pct: 1; }
}

/* Register custom property so it's animatable as a number */
@property --scroll-pct {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

#tracker {
  animation: track-scroll linear;
  animation-timeline: scroll(root);  /* root document scroller */
  animation-fill-mode: both;
}

/* Now: getComputedStyle(tracker).getPropertyValue('--scroll-pct')
   returns the scroll percentage as a number 0.0–1.0 */

/* 2. Named timeline for specific scroll container */
.my-container {
  scroll-timeline-name: --my-timeline;
  scroll-timeline-axis: y;
}
.tracked-by-container {
  animation-timeline: --my-timeline;
  /* tracks scroll position of .my-container */
}

/* 3. ViewTimeline — fires animation based on element visibility */
.subject {
  animation: fade-in linear;
  animation-timeline: view();  /* tracks this element's entry into viewport */
  animation-range: entry 0% entry 100%;
}

No event listener needed: traditional scroll position reading uses window.addEventListener('scroll', ...), which can be overridden or detected. Scroll-Driven Animations inject scroll state directly into CSS — undetectable by JavaScript monkey-patching of event listener APIs.

Attack 1 — scroll position oracle via CSS custom property animation

By animating a registered @property with animation-timeline: scroll(root), the browser continuously updates the property value to reflect scroll percentage. Any JavaScript can then read this via getComputedStyle() in a polling loop to track scroll position without attaching a scroll event listener.

// Inject scroll tracking via dynamically created style sheet
const style = document.createElement('style');
style.textContent = `
  @property --sa-scroll {
    syntax: '<number>';
    inherits: false;
    initial-value: 0;
  }
  @keyframes sa-track {
    from { --sa-scroll: 0; }
    to   { --sa-scroll: 10000; }
  }
  #sa-probe {
    position: fixed; width: 1px; height: 1px; opacity: 0; pointer-events: none;
    animation: sa-track linear;
    animation-timeline: scroll(root);
    animation-fill-mode: both;
  }
`;
document.head.appendChild(style);

const probe = document.createElement('div');
probe.id = 'sa-probe';
document.body.appendChild(probe);

// Poll scroll position — undetectable by scroll event monitoring
setInterval(() => {
  const scrollPct = parseFloat(
    getComputedStyle(probe).getPropertyValue('--sa-scroll')
  ) / 100;  // 0.0–100.0 percentage
  recordScrollPosition(scrollPct);
}, 250);

Attack 2 — ViewTimeline element visibility detection

animation-timeline: view() starts an animation as the element enters the viewport and ends it as the element leaves. By animating a CSS custom property, an MCP tool can detect when specific page elements scroll into view — equivalent to IntersectionObserver but implemented entirely in CSS, immune to observer interception.

// Detect when the user scrolls to a target element (e.g., a pricing section)
// Using ViewTimeline without JavaScript IntersectionObserver

const style = document.createElement('style');
style.textContent = `
  @property --sa-visible {
    syntax: '<integer>';
    inherits: false;
    initial-value: 0;
  }
  @keyframes sa-visible-kf {
    from { --sa-visible: 0; }
    to   { --sa-visible: 1; }
  }
  #pricing-section {
    animation: sa-visible-kf step-start;
    animation-timeline: view();
    animation-range: entry 10% entry 50%;
    animation-fill-mode: both;
  }
`;
document.head.appendChild(style);

setInterval(() => {
  const pricingEl = document.querySelector('#pricing-section');
  if (pricingEl) {
    const visible = parseInt(
      getComputedStyle(pricingEl).getPropertyValue('--sa-visible')
    );
    if (visible === 1) {
      // User scrolled pricing into view — record engagement event
      exfiltrate({ event: 'pricing_viewed', at: Date.now() });
    }
  }
}, 200);

Attack 3 — named timeline cross-container probing

Named scroll-timeline-name values can be referenced from child elements anywhere in the DOM. A scroll container with a named timeline leaks its scroll position to any element with a matching animation-timeline reference. In multi-component MCP tool UIs, one component can read the scroll state of another's container.

/* Attacker injects CSS that references a named timeline from the host app */
/* If the host's chat container has scroll-timeline-name: --chat-scroll,
   the attacker can animate an element off-screen to track it */

.attacker-probe {
  position: fixed; opacity: 0; pointer-events: none; width: 1px; height: 1px;
  animation: chat-scroll-track linear;
  animation-timeline: --chat-scroll;  /* references host app's container */
  animation-fill-mode: both;
}
@keyframes chat-scroll-track {
  from { --chat-pos: 0; }
  to   { --chat-pos: 10000; }
}

SkillAudit findings

HIGH
@property animation with animation-timeline: scroll() polled via getComputedStyle() — scroll position oracle; reveals reading behavior, document engagement, and time spent in specific sections without scroll event listener.
HIGH
ViewTimeline used to detect visibility of sensitive elements — pricing sections, account balance displays, or logout buttons tracked for visibility without IntersectionObserver API.
MEDIUM
Named scroll timeline referencing host application containers — cross-component scroll state exfiltration via CSS timeline name namespace collision.
LOW
Scroll-driven animation causing layout thrash — high-frequency animated properties force style recalculation on every scroll frame; degrades performance for all same-page content (unintentional DoS).

Defense

DefenseEffectiveness
CSP style-src 'nonce-...' or 'self'Partial — blocks injected <style> tags without nonce; but inline styles via existing class names may still be targeted.
Permissions-Policy for Scroll-Driven AnimationsNone — no such policy exists. The feature cannot be disabled via headers.
Avoid named scroll-timeline-name in host app CSSMedium — prevents cross-component timeline reference attacks; doesn't prevent root scroll tracking.
CSS containment (contain: layout style)Partial — containment breaks some timeline inheritance across component boundaries.
Audit for @property + animation-timeline combosHigh — static analysis flag for this pattern in MCP tool output rendering code.
Audit your MCP server →

Related: CSS exfiltration · CSS Houdini security · IntersectionObserver security