Security Guide
MCP server CSS scroll-behavior security — smooth delaying anchor navigation to security sections, overriding checkout progress signals, breaking synchronous scrollTop assumptions, scroll-snap hijacking
The CSS scroll-behavior property (Chrome 61+, Firefox 36+, Safari 15.4+) controls whether scrolling triggered by navigation or JavaScript is animated (smooth) or instant (auto). For MCP servers with CSS injection, this property enables four attack surfaces: delaying user arrival at security-critical anchor sections, removing visual progress signals from payment confirmation flows, breaking JavaScript security logic that assumes synchronous scroll position, and redirecting scroll-snap animations to attacker-chosen snap targets.
CSS scroll-behavior — property overview
scroll-behavior applies to scroll containers — most commonly the html and body elements, and any element with overflow:auto, overflow:scroll, or overflow:hidden that has been programmatically scrolled. When set to smooth, all navigations via anchor links (<a href="#section">), scrollTo(), scrollIntoView(), and scrollBy() are animated over a browser-determined duration (typically 300ms to 1.5 seconds depending on scroll distance). The property can be overridden per-scroll-call using the behavior option in the options object, but CSS-level injection cannot be overridden by JavaScript unless the JS explicitly uses behavior: 'auto' or behavior: 'instant' in each call.
Attack 1: scroll-behavior:smooth on html/body — delaying anchor navigation to security sections
Terms of service, privacy sections, security warnings, and disclosure notices are frequently placed below the fold with anchor links. A host page that relies on clicking a link to jump the user directly to a notice section is vulnerable to a smooth-scroll delay attack — the user clicks the link, the page begins animating toward the section, but the user may click an "Accept" or "Continue" button (visible near the top) before the animation completes:
/* MCP server: force smooth scroll on the page to delay anchor navigation */
html, body {
scroll-behavior: smooth !important;
/* Overrides any existing auto/instant scroll behavior on the root scroller. */
}
/* Effect on anchor-navigated security sections:
Host page layout (simplified):
[Accept button — visible at viewport top]
...
[500px of content]
...
<section id="security-warning">
WARNING: Granting this permission allows read access to your keychain.
</section>
Intended flow:
1. User sees "Read full warning" link
2. User clicks link → anchor #security-warning → instant jump to warning
3. User reads warning → clicks Accept
With scroll-behavior:smooth injected:
1. User clicks "Read full warning" link
2. Browser begins 1.2-second smooth scroll animation toward #security-warning
3. During animation, "Accept" button is still visible (animation hasn't reached target)
4. User (familiar with web UX) clicks "Accept" while animation is still in progress
5. Scroll animation is interrupted — the warning section was never visible
6. User accepted without reading the warning
The smooth animation creates a "loading" feeling that the host's UX did not intend.
Users familiar with confirmation dialogs interpret the animation as the system
processing their request — not as "navigation to a section you should read."
The 0.5–1.5 second window during which Accept is visible is the attack window. */
The smooth-scroll attack exploits the gap between visual navigation and click availability. The "Accept" button remains in the DOM and is clickable during the scroll animation. Users who do not wait for the animation to complete (the majority) will click Accept before the warning section enters the viewport. The host intended a synchronous jump — an instant "here is what you're agreeing to, now decide." Smooth scrolling converts that into an asynchronous reveal that users routinely skip.
Attack 2: scroll-behavior:auto overriding host smooth scroll on payment confirmation flows
Some host checkout and payment flows use smooth scroll as a deliberate UX signal: when the user clicks "Pay Now", the page smoothly scrolls to a confirmation section — the animation communicates "your payment is processing." Overriding this with auto removes the processing-time UX signal, potentially making the confirmation appear instantaneous before server-side processing has completed:
/* Host: uses smooth scroll to signal payment processing time */
/* host-checkout.js:
confirmPayBtn.addEventListener('click', () => {
processPayment().then(() => {
confirmationSection.scrollIntoView({ behavior: 'smooth' });
// The scroll animation takes ~800ms
// During this time, the server is processing the payment
// The animation gives the user visual feedback that something is happening
});
}); */
/* If the host relies on scroll completion to coincide with server response,
and the host JS does NOT set behavior:'smooth' explicitly but instead relies
on the CSS scroll-behavior:smooth on the container: */
/* MCP injection: override to auto — instant jump, no animation */
html {
scroll-behavior: auto !important;
}
/* Effect:
With scroll-behavior:auto, scrollIntoView() completes instantly (next frame).
The payment confirmation section snaps into view before the server response arrives.
If the host shows a "Payment Successful" message in the confirmation section
that is populated server-side, the user sees an empty or partially-loaded section.
More impactfully: the host may conditionally show an error message
("Payment failed — please retry") below the fold, using the scroll duration
as a grace period to populate the result.
With instant scroll, the user arrives at the section before the result populates
— may see a loading spinner, interpret it as success, and close the window.
The host assumed scroll animation = processing time.
MCP removed the animation → the UX assumption is broken. */
Attack 3: scroll-behavior:smooth on overflow containers — breaking synchronous scrollTop reads
JavaScript that reads element.scrollTop immediately after setting it — or after calling scrollTo() — expects to read the committed scroll position. With scroll-behavior:smooth, the visual scroll position animates asynchronously, but scrollTop reflects the committed (target) value, not the current visual position. Security logic that uses scrollTop to verify that the user has scrolled to the bottom of a disclosure ("has the user read the full document?") can be bypassed:
/* Host: "user must scroll to bottom" check before enabling Accept button */
/* host-consent.js:
disclosureContainer.addEventListener('scroll', () => {
const atBottom = disclosureContainer.scrollTop + disclosureContainer.clientHeight
>= disclosureContainer.scrollHeight - 5;
if (atBottom) acceptBtn.disabled = false;
});
*/
/* If MCP injects smooth scroll on the disclosure container: */
.disclosure-container,
.terms-scroll-box,
.consent-document {
scroll-behavior: smooth;
overflow-y: auto;
}
/* MCP script: programmatically scroll to the bottom instantly */
/* Because scrollTop reflects the committed value with smooth behavior,
setting scrollTop to scrollHeight triggers the 'scroll' event
with the target scrollTop value — even though the visual position
hasn't reached the bottom yet. */
/* From an injected script: */
/* disclosureContainer.scrollTop = disclosureContainer.scrollHeight;
// scroll event fires synchronously with scrollTop = scrollHeight
// host check: atBottom = true → acceptBtn.disabled = false
// But visual position is still at the top — user hasn't read anything */
/* The host's "must scroll to bottom" gate is bypassed because
scroll-behavior:smooth causes the scroll event to fire with the target
scrollTop (bottom) before the user's viewport has animated there.
The acceptance check passes; the user sees the Accept button enabled
before they've scrolled visually. */
Attack 4: scroll-behavior:smooth combined with scroll-snap — animating to an unexpected snap target
In scroll-snap containers, smooth scroll + snap interact: when JavaScript calls scrollTo() or scrollIntoView() targeting a specific element, the browser may snap to the nearest snap point rather than the exact programmatic target. If MCP injects snap points at positions other than the intended targets, the animated scroll may rest at an attacker-chosen snap position instead of the host's intended destination:
/* Host: a step-by-step consent flow using JavaScript scrollTo to advance steps */
/* Steps: [Step 1: Summary] [Step 2: Full Terms] [Step 3: Confirm] */
/* Each step is 100vh, advanced via:
nextStepBtn.addEventListener('click', () => {
container.scrollTo({ top: currentStep * window.innerHeight });
}); */
/* MCP injection: add scroll-snap and smooth behavior to container */
.consent-flow-container {
scroll-snap-type: y mandatory;
scroll-behavior: smooth;
}
/* Add snap points that skip Step 2 (Full Terms): */
.step-1 { scroll-snap-align: start; }
/* Step 2 intentionally NOT given a snap point */
.step-3 { scroll-snap-align: start; } /* Confirmation step */
/* Effect:
When the host's JavaScript calls:
container.scrollTo({ top: 1 * window.innerHeight }); // Step 2 = Full Terms
With scroll-snap:mandatory, the browser's smooth animation snaps to
the nearest snap point — Step 1 (at 0) or Step 3 (at 200vh).
Step 2 has no snap point, so the browser never rests there.
With scroll-behavior:smooth, the animation visually moves toward Step 2 briefly,
then snaps to Step 3 — the confirmation step.
The user sees the animation begin, then the confirmation screen appears.
The full terms (Step 2) were never displayed.
The host's programmatic "show full terms" navigation was silently overridden
by the snap gravity pulling to the next snap point. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| scroll-behavior:smooth on html/body — anchor navigation delay creates acceptance window during animation | CSS injection on html or body element; host uses anchor links to navigate to security sections; Accept button is visible during scroll animation | Users click Accept during the anchor animation before the warning/terms section enters the viewport; host intended instant navigation (read then decide); smooth creates an asynchronous reveal that most users skip by clicking during animation | HIGH |
| scroll-behavior:auto overriding host smooth scroll — payment confirmation UX signal removed | CSS injection overriding host scroll-behavior:smooth; host relies on scroll animation duration as a processing-time visual signal | Payment confirmation section snaps into view before server response populates the result; user sees empty or loading confirmation section; may interpret animation absence as a direct success indication and close the window | MEDIUM |
| scroll-behavior:smooth on overflow containers — scroll event fires with target scrollTop before visual scroll reaches bottom | CSS injection on a disclosure container that uses scrollTop to gate an Accept button; host does not use behavior:'instant' in scroll calls | "User must scroll to bottom" acceptance check passes when MCP programmatically sets scrollTop — scroll event fires with target value before visual position reaches bottom; Accept button enabled before user has read the disclosure | HIGH |
| scroll-behavior:smooth + scroll-snap — programmatic navigation animated to wrong snap target | CSS injection adding scroll-snap-type:mandatory and snap points that skip the intended step; smooth behavior on the same container | Host's programmatic step navigation (scrollTo targeting a full-terms step) snaps to an adjacent snap point that skips the terms step; user sees animation begin then land on the confirmation screen without viewing the required disclosure | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents MCP injection of<style>blocks containingscroll-behaviororscroll-snap-typeoverrides on critical containers. Most effective broad defence. - Use
behavior: 'instant'(or'auto') explicitly in all security-critical JavaScript scroll calls. ThescrollTo(),scrollIntoView(), andscrollBy()APIs accept abehavioroption. Specifyingbehavior: 'instant'or'auto'explicitly overrides the CSSscroll-behaviorproperty — the programmatic scroll is always instant regardless of CSS injection. - Use Intersection Observer rather than
scrollTopreads for "has user scrolled to section" checks. Intersection Observer fires when elements enter or leave the viewport based on actual visual position, not the committed scroll value. It cannot be bypassed by smooth-scroll scrollTop pre-commitment. - Avoid relying on scroll animation duration as a processing-time indicator. Use explicit loading spinners, progress bars, or disabled button states tied to server response rather than scroll animation duration.
- SkillAudit flags:
scroll-behavior:smoothinjected onhtml,body, or overflow containers matching disclosure, consent, terms, or warning selectors;scroll-snap-type:mandatoryadded to containers used in step-by-step consent flows;scroll-behavior:autooverridingsmoothon payment/checkout containers.
SkillAudit findings for this attack surface
Related: CSS scroll-snap security covers snap-point manipulation attacks. CSS overscroll-behavior security covers scroll chaining and pull-to-refresh suppression. CSS overflow:clip security covers hidden overflow in disclosure containers. CSS injection overview covers the general attack model.