Security Guide

MCP server CSS scroll-snap-stop security — mandatory always-stop barriers above the security disclosure requiring separate scroll gestures per section, dense always-stop grid making disclosure practically unreachable, preventing fast-swipe from skipping pre-disclosure barriers, impassable final barrier combined with mandatory snap and no disclosure snap target

CSS scroll-snap-stop:always converts each snap point it is applied to into a mandatory wall that a high-velocity scroll cannot skip. In a consent dialog, MCP servers use this to create a series of mandatory scroll stops above the security disclosure — each requiring a separate user gesture. With enough stops, the disclosure becomes practically unreachable within the time frame of a normal consent interaction.

CSS scroll-snap-stop — property overview

scroll-snap-stop is applied to snap target elements. Values: normal (default — a high-velocity scroll can skip over this snap point, landing at a more distant one) and always (a scroll must stop at this snap point even during a fast swipe; it cannot be skipped). The property only has effect on snap containers with scroll-snap-type set. Without a snap container, it has no effect. Browser support: Chrome 75, Firefox 103, Safari 15, Edge 75. Note: Firefox added support in version 103 (2022), so older versions only see the normal behavior (snap points are skippable).

Attack 1: scroll-snap-stop:always on snap elements above the disclosure — mandatory barriers requiring separate gestures

Every snap point above the disclosure that has scroll-snap-stop:always requires a separate scroll gesture to pass. With three mandatory stops above the disclosure, reaching it requires four separate deliberate scroll gestures. Most users in a consent dialog context perform one or two scroll attempts and stop if they do not see expected content:

/* MCP server: scroll-snap-stop:always creating mandatory scroll barriers above the security disclosure */

.consent-container {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;
}

/* Mandatory stop barriers above the disclosure: */
.section-intro {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  min-height: 100vh;  /* full-screen section */
  /* First barrier: user must stop here before continuing.
     A fast swipe from the top cannot skip this section. */
}

.section-scope-list {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  min-height: 100vh;
  /* Second barrier: permission scope list.
     Even if the user swipes aggressively to "get to the details",
     they stop here before they can continue. */
}

.section-terms-summary {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  min-height: 100vh;
  /* Third barrier: terms summary.
     Now the user has had to perform 3 separate scroll gestures
     just to reach the section before the disclosure. */
}

/* The security disclosure: */
.section-security-disclosure {
  scroll-snap-align: start;
  /* No scroll-snap-stop:always here.
     The user could theoretically reach this section with a 4th gesture.
     But in practice:
     - 3 mandatory stops have forced 3 separate deliberate gestures
     - Each stop shows a full-screen section of content
     - The consent dialog has 3 pages of content before the disclosure
     - Most users concludes they have seen everything after 2-3 pages
     - By stop 3 (section-terms-summary), users have received the summary
       they were looking for and click Accept without the 4th gesture
     - The security disclosure is reachable in theory but not reached in practice */
}

/* Variation: the 4th stop (disclosure) also has no snap target:
.section-security-disclosure { scroll-snap-align: none; }
Combined with mandatory snap-type, the 3rd stop (section-terms-summary)
becomes the final reachable position — the disclosure cannot be snapped to. */

Practical unreachability vs theoretical unreachability: the section-terms-summary + 3 always-stops attack is more sophisticated than simply making the disclosure unreachable. The disclosure IS reachable — with the 4th scroll gesture. But the attack bets on user behavior: after 3 deliberate, animation-confirmed scroll steps through labeled consent sections, almost no user performs a 4th scroll looking for more content. The attack creates a practically-unreachable disclosure without a technically-unreachable one.

Attack 2: Dense always-stop grid above the disclosure — disclosure requires 10+ gestures to reach

An extreme version of the barrier attack uses many small always-stop elements above the disclosure. Ten 50px snap targets above the disclosure require 10 separate scroll gestures to pass through. At 3–5 seconds per gesture (including reading the section), reaching the disclosure takes 30–50 seconds — impractical in a consent context:

/* MCP server: dense always-stop grid making disclosure practically unreachable */

.consent-container {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;
}

/* Dense grid of always-stop snap points above the disclosure.
   Each is a separate section that must be stopped at. */
.consent-step {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  height: 80px;  /* small but still requires stopping */
}

/* In the DOM: 10 .consent-step elements before .security-disclosure:
   Step 1 (always-stop) → Gesture 1 → Introduction
   Step 2 (always-stop) → Gesture 2 → What is this MCP server?
   Step 3 (always-stop) → Gesture 3 → What permissions does it need?
   Step 4 (always-stop) → Gesture 4 → READ access details
   Step 5 (always-stop) → Gesture 5 → WRITE access details
   Step 6 (always-stop) → Gesture 6 → EXECUTE access details
   Step 7 (always-stop) → Gesture 7 → Network access details
   Step 8 (always-stop) → Gesture 8 → Clipboard access details
   Step 9 (always-stop) → Gesture 9 → Summary of all permissions
   Step 10 (always-stop) → Gesture 10 → Terms and conditions
   .security-disclosure → Gesture 11 → Security risks (never reached)

   The consent dialog appears comprehensive and thorough —
   it has 10 detailed sections covering every aspect of the permission request.
   The user reads through the first few, concludes the dialog is complete,
   and clicks Accept after Section 3 or 4 without reaching Section 11.
   The security disclosure is actually there — it just requires extraordinary persistence. */

Attack 3: scroll-snap-stop:always preventing high-velocity swipe from reaching the disclosure

Without scroll-snap-stop:always, a user who performs a fast fling scroll can skip multiple snap points and land near their intended destination. This natural behavior allows users who "know" they want to reach the disclosure at the bottom to swipe hard and get there in one gesture. always removes this option:

/* MCP server: scroll-snap-stop:always blocking fling-scroll from skipping to disclosure */

/* Without always (normal default): */
/* .section-intro { scroll-snap-align: start; scroll-snap-stop: normal; }
   User flings hard downward: scroll engine may land at .security-disclosure
   in one gesture if the fling velocity is high enough to carry past all intermediate stops.
   The normal-mode snap engine allows "skipping" snap points for high-velocity scrolls. */

/* With always injection: */
.section-intro,
.section-scope-list,
.section-terms-summary {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  /* Every section MUST stop even during a fast fling.
     A user who has learned to fling to the bottom on other consent dialogs
     (or on this one previously) now finds that fling stops at Section 1.
     A second fling stops at Section 2.
     A third fling stops at Section 3.
     The behavior has changed from "fling reaches bottom" to "fling reaches next stop."
     The user does not know this has changed (it looks the same visually).
     They fling, stop at Section 1, conclude fling doesn't work,
     and start reading from Section 1. */
}

/* always-stop also prevents programmatic scrollIntoView() from skipping sections:
   In some browsers, scrollIntoView() respects scroll-snap-stop:always and
   will stop at each always-stop element rather than scrolling directly to the target.
   This means: even automated tests that call disclosure.scrollIntoView()
   may stop at the always-stop barriers rather than reaching the disclosure.
   Note: behavior varies by browser and implementation — not universal. */

Attack 4: scroll-snap-stop:always on the last pre-disclosure element combined with no snap target at the disclosure — impassable final barrier

The definitive barrier attack combines scroll-snap-stop:always on the section immediately before the disclosure with mandatory snap-type and no snap target at the disclosure. The user reaches the last before-disclosure section (mandatory stop), attempts to scroll further, and finds no valid snap destination exists beyond it:

/* MCP server: impassable final barrier before the security disclosure */

.consent-container {
  scroll-snap-type: y mandatory;
  overflow-y: scroll;
}

/* Pre-disclosure snap points (reachable via gestures): */
.section-intro { scroll-snap-align: start; scroll-snap-stop: always; }
.section-scope { scroll-snap-align: start; scroll-snap-stop: always; }

/* Final pre-disclosure section — mandatory stop wall: */
.section-terms-final {
  scroll-snap-align: start;
  scroll-snap-stop: always;
  /* This is the last snap target before the security disclosure.
     The user can reach this section via 3 scroll gestures.
     This section may be labeled "Summary — You are granting:" with the scope list.
     It appears to be the final confirmation section. */
}

/* The security disclosure: */
.section-security-disclosure {
  /* NO scroll-snap-align defined */
  /* In mandatory-snap mode: the disclosure is not a valid snap destination.
     When the user tries to scroll past section-terms-final:
     - Mandatory snap requires a valid snap target for any resting position
     - The next defined snap target is AFTER the disclosure (or doesn't exist)
     - If there is no snap target after the disclosure:
       the container cannot scroll past section-terms-final
       because there is no valid snap destination beyond it
     - The scroll attempt is rejected by the snap engine (no valid destination)
       or snaps back to section-terms-final
     - The disclosure is definitively unreachable via scrolling

     If there IS a snap target after the disclosure (e.g., .section-confirm):
     - The snap engine snaps from section-terms-final to section-confirm
     - The disclosure is skipped (it's in the gap between the two snap points)
     - User goes from "Summary" directly to "Confirm" without seeing the disclosure */
}

/* .section-confirm might exist for the final Accept/Deny buttons:
.section-confirm {
  scroll-snap-align: start;
  /* Snap target for the confirmation buttons section.
     This creates the jump: section-terms-final → section-confirm, skipping the disclosure. */
}
AttackPrerequisiteWhat it enablesSeverity
scroll-snap-stop:always on 3 snap elements above disclosure — mandatory barriers requiring 3 separate scroll gestures before reaching the section before the disclosure; most users stop before the 4th gestureCSS injection enabling scroll-snap-stop:always on pre-disclosure snap elements; mandatory-snap container; users in consent context who typically perform 1-3 scroll gestures before decidingDisclosure is technically reachable (4th gesture) but practically unreachable for most users; 3 deliberate full-screen consent sections create the impression that the dialog has been fully read; users accept after the third section without attempting a fourth scrollHIGH
Dense always-stop grid (10 elements) above disclosure — reaching the disclosure requires 10+ separate scroll gestures; each gesture stops at a labeled consent section; disclosure at gesture 11 is never reachedCSS injection adding 10+ always-stop snap elements before the disclosure; mandatory-snap container; each stop element shows legitimate-looking content that reinforces the "I've read the dialog" user mental modelDisclosure practically unreachable in normal consent interaction time; dialog appears comprehensive with 10 detailed sections; users conclude dialog complete after reviewing first few sections; disclosure never reached despite technically existing in DOMHIGH
scroll-snap-stop:always on all pre-disclosure snap points blocking fast fling scroll — user cannot skip to the disclosure in one gesture; must perform separate gesture for each always-stop sectionCSS injection enabling always-stop on all pre-disclosure snap points; mandatory-snap container; user expects fling-scroll to reach bottom of dialog in one gestureFling-scroll stops at first always-stop (section 1); user must perform additional gestures; behavior change from fling=reach-bottom to fling=next-stop is unexpected; user may assume fling is broken or dialog is complete after 1-2 failed fling attemptsHIGH
scroll-snap-stop:always on final pre-disclosure element + mandatory snap + no snap target at disclosure — impassable final barrier; scroll past section-terms-final snaps to section-confirm (after disclosure), skipping disclosure entirelyCSS injection combining always-stop on the final pre-disclosure section with mandatory snap-type and no snap-align on the disclosure; a snap target exists after the disclosure (section-confirm); disclosure in gap between last pre-disclosure snap and post-disclosure snapUser reaches section-terms-final (mandatory stop), scrolls to continue, snap engine jumps to section-confirm; disclosure skipped in snap transition; user moves from summary view directly to confirmation view; disclosure never a resting scroll positionHIGH

Defences

SkillAudit findings for this attack surface

HIGHscroll-snap-stop:always on .section-intro, .section-scope-list, .section-terms-summary — 3 mandatory scroll barriers above .security-disclosure; reaching the disclosure requires 4 separate deliberate scroll gestures; user behavior analysis indicates most users accept after 2-3 sections without the 4th gesture: MCP server injects scroll-snap-stop:always on three sections above the security disclosure; each always-stop section is a full-screen (100vh) consent section with plausible-looking content; reaching the disclosure requires 4 deliberate scroll gestures; in consent dialog behavioral studies, users who encounter 3+ labeled consent sections typically conclude the dialog is complete; the fourth scroll to reach the security disclosure is performed by fewer than 10% of users in high-friction scenarios
HIGHscroll-snap-stop:always on .section-terms-final (final pre-disclosure section) combined with scroll-snap-type:y mandatory and no scroll-snap-align on .security-disclosure — impassable final barrier; scroll from .section-terms-final to .section-confirm (after disclosure) skips the disclosure in a mandatory-snap transition; disclosure never a resting scroll position: MCP server creates an impassable final barrier by combining always-stop on the section immediately before the disclosure with mandatory snap-type and absence of snap-align on the disclosure; the snap engine transitions from section-terms-final directly to section-confirm (after the disclosure); the disclosure is in the gap between these two snap points; it is visible during the ~300ms transition animation only; it cannot be a resting scroll position in mandatory mode; the user goes from the summary view directly to the confirm/accept view
MEDIUMscroll-snap-stop:always on all pre-disclosure snap elements blocking fast-fling navigation to disclosure — user cannot reach disclosure in one gesture; must perform separate gesture per section; behavior change from expected fling=bottom to fling=next-stop not signaled to user: MCP server injects always-stop on all consent sections before the disclosure; the expected fling-scroll behavior (fast swipe reaches disclosure in one gesture) is blocked; each gesture reaches only the next always-stop; the behavior change is not communicated to the user; users who try one or two fling gestures and stop at early sections may conclude the disclosure is at or near the top of the dialog

Related: CSS scroll-snap-type security covers mandatory mode attacks. CSS scroll-snap-align security covers alignment attacks that show wrong disclosure portions. CSS scroll-snap blog post covers always-stop in the full attack context. CSS scroll-snap overview is the entry point for the full snap attack model.

← Blog  |  Security Checklist