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. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| 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 gesture | CSS 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 deciding | Disclosure 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 scroll | HIGH |
| 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 reached | CSS 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 model | Disclosure 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 DOM | HIGH |
| 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 section | CSS injection enabling always-stop on all pre-disclosure snap points; mandatory-snap container; user expects fling-scroll to reach bottom of dialog in one gesture | Fling-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 attempts | HIGH |
| 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 entirely | CSS 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 snap | User 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 position | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents injection of CSS that enables scroll-snap-stop:always. The complete solution. - Count mandatory scroll gestures required to reach the disclosure. After rendering with MCP styles, simulate the scroll experience: programmatically scroll forward one snap point at a time, counting gestures. If reaching the security disclosure requires more than 2 deliberate scroll gestures, flag as a friction-injection attack on disclosure reachability.
- Detect scroll-snap-stop:always on elements before the disclosure. Parse all snap elements in the consent container. Check whether any element with
scroll-snap-stop:alwaysis positioned before the security disclosure in the scroll order. More than two such elements above the disclosure is a HIGH severity finding. - Verify fast-scroll reachability. Programmatically simulate a high-velocity scroll to the bottom of the consent container. If the container does not reach the disclosure (due to always-stop barriers), flag as a scroll friction attack.
- Freeze scroll-snap-stop:normal !important on pre-disclosure snap elements. Set
scroll-snap-stop: normal !importanton all consent sections in the host stylesheet. This allows fling-scroll to skip sections and reach the disclosure in one gesture. - SkillAudit flags: 3+
scroll-snap-stop:alwayselements in a consent container before the security disclosure; anyscroll-snap-stop:alwayselement positioned immediately before the disclosure combined with noscroll-snap-alignon the disclosure; total mandatory-stop count above disclosure exceeding 2.
SkillAudit findings for this attack surface
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.