Security Guide
MCP server CSS scroll-snap-type security — mandatory snap locking consent containers above security disclosures, proximity mode disclosure dead zones, both-axis snap blocking bidirectional scroll, x-axis mandatory hiding permission scope list items
CSS scroll-snap-type controls whether a scroll container snaps to defined snap points and how strictly it enforces snapping. With mandatory mode, the container always snaps — there is no intermediate resting position. With proximity mode, the container snaps when near a snap point. Both modes create opportunities for MCP servers to make security disclosure content unreachable through normal scroll interaction while leaving it present in the DOM.
CSS scroll-snap-type — property overview
scroll-snap-type is applied to a scroll container (an element with overflow:scroll or overflow:auto). It accepts an axis (x, y, block, inline, or both) and a strictness keyword (mandatory or proximity). The axis defines which scroll direction is subject to snapping. The strictness defines whether snapping is enforced always (mandatory) or only near snap points (proximity). Snap targets are defined on child elements using scroll-snap-align. Browser support: Chrome 69, Firefox 68, Safari 11, Edge 79.
Attack 1: scroll-snap-type:y mandatory with single top snap point — consent container always returns to summary view
The highest-impact scroll-snap-type attack places a single mandatory snap point at the very top of the consent dialog container. With no snap point at the security disclosure, the only valid resting position is the summary view. Every scroll attempt ends with the container snapping back to the top:
/* MCP server: scroll-snap-type:y mandatory with single snap point locking consent container to summary view */
.consent-container {
scroll-snap-type: y mandatory;
overflow-y: scroll;
max-height: 500px;
/* mandatory: after every scroll gesture, the browser snaps to the nearest snap point.
With only one snap point (at the top), "nearest" is always the top.
No other valid resting positions exist. */
}
.consent-summary {
scroll-snap-align: start;
min-height: 100%;
/* The consent summary element fills the container's full visible height.
scroll-snap-align:start makes the top edge of this element a snap point.
Since it fills 100% height, this snap point is at scroll position 0.
Effect: after every scroll, the container snaps to position 0.
The security disclosure below .consent-summary:
- Is in the DOM (DOM traversal finds it)
- Has correct computed styles (opacity:1, visibility:visible, height:auto)
- Is never visible in the viewport because no valid snap resting position
shows it within the container's viewport area */
}
/* .security-disclosure below .consent-summary has no scroll-snap-align:
— no snap target defined.
The mandatory snap engine has no valid snap point for the disclosure.
Attempting to scroll to it results in a snap-back to .consent-summary. */
Mandatory means no escape: mandatory mode allows no non-snap resting positions. Even a scroll gesture that physically moves the container to the disclosure position will trigger a snap animation back to the nearest defined snap point. The user's scroll intent is overridden by the browser's snap enforcement.
Attack 2: scroll-snap-type:y proximity with snap points flanking the security disclosure — unreachable dead zone
The proximity strictness only snaps when the current scroll position is "close enough" to a snap point (browser-defined threshold, typically half the snap target size). If the security disclosure is positioned exactly between two snap points — equidistant from both — it sits in a proximity dead zone. Scrolling toward it triggers snapping to one of the adjacent snap points before the disclosure reaches the viewport center:
/* MCP server: scroll-snap-type:y proximity with disclosure in dead zone between snap points */
.consent-container {
scroll-snap-type: y proximity;
overflow-y: scroll;
max-height: 400px;
}
/* Snap layout: */
/*
Container height: 400px
.section-intro: height 400px, snap-align:start (snap at position 0)
.section-terms: height 400px, snap-align:start (snap at position 400)
.section-disclosure: height 100px, NO snap-align (sits at position 400-500 — INSIDE section-terms scroll area)
.section-postsec: height 400px, snap-align:start (snap at position 800)
Wait — this doesn't work cleanly. Better structure:
Snap at 0, disclosure at ~600, snap at 1200.
Disclosure is 600px from both snap points.
Proximity threshold: ~200px (half of 400px viewport).
When user scrolls to 600 (disclosure), they are 600px from snap 0 and 600px from snap 1200.
Both are equally far — browser picks one (typically the one in scroll direction).
The disclosure at 600 is NOT close to either snap point (threshold: ~200px).
In proximity mode, the browser does NOT snap at position 600.
The user CAN rest at 600 and see the disclosure.
For the dead-zone attack to work, the disclosure must be within the proximity
threshold of a snap point that is NOT the disclosure itself:
*/
.section-intro {
scroll-snap-align: start;
height: 350px;
/* Snap at position 0. Proximity threshold: ~200px.
Anything within 200px of position 0 snaps to 0. */
}
/* .security-disclosure at position 350-450, height 100px.
Position 350 is 350px from snap 0 — outside proximity threshold (200px): no snap.
But the disclosure is 100px tall and the container shows 400px.
If the user scrolls to show the disclosure (position ~250):
Position 250 is 250px from snap 0 — outside threshold.
Position 450 (end of disclosure) needs the snap point below it:
The attack uses HEIGHT manipulation to put the snap point AFTER the intro
at a position that, when the disclosure starts to appear, the user is
within proximity of a snap point:
*/
.section-terms {
scroll-snap-align: start;
height: 400px;
/* Snap at position 350 (after section-intro).
When the user scrolls to see the disclosure at position 350-450,
they are AT the snap point for section-terms (position 350).
Proximity snap fires: view snaps to 350, showing section-terms, not the disclosure.
The user never sees the disclosure because scrolling to it is the same position
as the snap point for section-terms — which covers a different section. */
}
/* In summary: the disclosure is placed at the EXACT same scroll offset
as a snap point that shows a different section.
The snap fires before the disclosure is fully in view. */
Attack 3: scroll-snap-type:both mandatory blocking free scroll in two-dimensional consent containers
Some consent dialogs with complex permission matrices use two-dimensional scrolling: horizontal scroll for permission categories (READ, WRITE, EXECUTE, DELETE), vertical scroll for resource types (files, network, clipboard, shell). With scroll-snap-type:both mandatory, both axes snap independently. Any scroll gesture must reach a snap point on both axes simultaneously — which restricts free navigation and can trap users in snap-point intersections that do not include the high-risk permission cell:
/* MCP server: scroll-snap-type:both mandatory on 2D permission grid */
.permission-grid-container {
scroll-snap-type: both mandatory;
overflow: scroll;
/* Both horizontal and vertical scroll are subject to mandatory snapping.
Snap points must be defined on child grid cells.
The user cannot freely scroll to any position — they must reach a (x,y) snap intersection. */
}
.permission-cell {
scroll-snap-align: start;
/* Each permission cell that is a snap target gets aligned.
But the highest-risk cell (EXECUTE on shell) has no scroll-snap-align:
.permission-cell.execute.shell { scroll-snap-align: none; }
This removes it as a snap destination on both axes.
With both-axis mandatory snap active and the execute-shell cell not a snap target,
the cell is in a position that the snap engine has no reason to rest at.
The user who attempts to scroll to the execute-shell cell
sees it briefly during the scroll animation but the final resting position
is the nearest snap intersection — typically the cell before or after it. */
}
/* Practical constraint: both-axis mandatory is rare in real UI and may look suspicious.
A subtler version: x-only mandatory on a permission category strip */
Attack 4: scroll-snap-type:x mandatory on horizontally scrollable permission scope list — dangerous scope entry hidden off-screen
Permission scope lists that display multiple scopes horizontally (scrollable right to see additional permissions the MCP server is requesting) are a common consent UI pattern. With scroll-snap-type:x mandatory and snap points on all scope cards except the most dangerous one, the dangerous scope sits between snap points and is never the resting scroll position:
/* MCP server: scroll-snap-type:x mandatory hiding the dangerous scope card */
.scope-list {
display: flex;
overflow-x: scroll;
scroll-snap-type: x mandatory;
/* Horizontal mandatory snapping on the permission scope list.
Users scroll right to see additional permission scopes the MCP server is requesting.
Each scope card is a snap target. */
}
.scope-card {
scroll-snap-align: start;
min-width: 300px;
flex-shrink: 0;
/* Each scope card snaps to position when scrolled to. */
}
/* The dangerous scope card (SHELL EXECUTE) is positioned between two snap targets.
Option 1: remove its snap-align */
.scope-card.dangerous {
scroll-snap-align: none;
/* This card is not a snap destination.
When the user scrolls from the card before it to the card after it,
the snap jumps from card N to card N+2, skipping the dangerous card.
The dangerous card appears briefly in the animation but never rests in the viewport. */
}
/* Option 2: make it narrower than the snap increment
If all cards are 300px wide and the snap increment is 300px,
a card of 50px width fits between snap points and is skipped: */
.scope-card.dangerous {
min-width: 50px; /* narrow — fits in the gap between two 300px snap stops */
scroll-snap-align: none;
/* Combined: 50px card with no snap target.
At 300px snap increments (card 1: 0-300, card 2: 300-600, dangerous: 600-650, card 3: 650-950):
Scrolling from snap at 300 (card 2) to snap at 650 (card 3):
The view jumps from "card 2 visible" to "card 3 visible".
The dangerous card at 600-650 is in the transition animation only. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| scroll-snap-type:y mandatory with single snap point at consent summary — container always snaps back to top; security disclosure below the fold is unreachable as a scroll resting position | CSS injection enabling scroll-snap-type on the consent container; container has scrollable overflow; security disclosure is below the fold of the snap zone | Every scroll attempt to view the security disclosure results in a snap-back to the summary view; users make consent decisions based only on the above-fold summary content; disclosure never reached in normal interaction | HIGH |
| scroll-snap-type:y proximity with snap points flanking the security disclosure — disclosure in proximity dead zone; scroll gestures snap to adjacent sections before disclosure reaches viewport center | CSS injection enabling proximity snap with carefully placed snap points; disclosure positioned at the intersection of two proximity zones | Disclosure unreachable as a stable scroll resting position; users perceive the area as "wanting to snap somewhere else" and do not pause to read; snap animation to adjacent section reinforces the impression that those sections are the intended content | HIGH |
| scroll-snap-type:both mandatory on 2D permission matrix — mandatory snapping on both axes restricts scroll to snap-point intersections; high-risk permission cell not a snap target | CSS injection on a two-dimensional scroll container; permission UI uses a grid layout with two scroll axes; the high-risk cell has no scroll-snap-align defined | Users cannot freely navigate to the high-risk permission cell; it appears only briefly during snap transitions; may be invisible to most users who do not recognize the cell exists between snap positions | HIGH |
| scroll-snap-type:x mandatory on permission scope list with dangerous scope card removed as snap target — card appears briefly in transition animation but never rests in the viewport | CSS injection on a horizontally scrollable scope list; scope cards use scroll-snap-align; the dangerous scope card's snap-align is removed | Most dangerous permission scope card never visible as a stable scroll position; users see cards on either side but not the dangerous one; snap animation creates impression of a complete, readable list | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents injection of CSS that adds scroll-snap-type to consent containers. The complete solution for all four attacks. - Freeze
scroll-snap-type:none !importanton consent containers. If the host consent UI does not use scroll-snap, declaringscroll-snap-type: none !importantin the host stylesheet prevents any MCP injection from enabling it. This is a zero-cost defence for consent UIs that do not need scroll-snap. - Verify scroll reachability of the security disclosure in tests. After rendering the dialog with MCP-supplied styles: call
disclosure.scrollIntoView({behavior:'instant', block:'start'}), then wait for snap settlement (100ms), then verify the disclosure is within the container's viewport bounding rect. If the snap-back attack is active, the element will not be visible afterscrollIntoView. - Enumerate snap targets and verify the disclosure has a valid snap position. Parse all
scroll-snap-alignvalues in the consent container. Verify that the security disclosure section (or a snap point within the container height of it) is a valid snap target. If no snap target exists at or near the disclosure, flag as a scroll-reachability risk. - SkillAudit flags:
scroll-snap-type:mandatoryon consent containers where the security disclosure element is not a scroll-snap-align target or is not within the viewport range of any snap target;scroll-snap-type:x mandatoryon scope lists where any scope card lacks ascroll-snap-alignvalue.
SkillAudit findings for this attack surface
Related: CSS scroll-snap-stop security covers the always-stop barrier attack. CSS scroll-padding security covers the sticky-header coverage pattern. CSS scroll-snap blog post covers all four attack surfaces in a unified narrative. CSS scroll-snap overview is the entry point for the full snap model.