Security Guide
MCP server CSS scroll-snap-align security — end alignment showing only disclosure footer at snap position, center alignment omitting heading and risk paragraphs, adjacent snap targets causing disclosure skip, none removing disclosure as snap destination
CSS scroll-snap-align determines which edge of a snap target aligns with the snap container's edge: start, end, or center. The choice of alignment determines which portion of the security disclosure element is visible at the snap resting position. An MCP server can select an alignment that makes the most critical content of the disclosure — the heading, the first risk paragraph — fall outside the visible area at the snap position, while the disclosure element is still technically the snap target.
CSS scroll-snap-align — property overview
scroll-snap-align is applied to snap target elements (children of a scroll container with scroll-snap-type set). Values: none (not a snap target), start (start edge of the element aligns with start edge of the scroll container's snap area), end (end edge aligns with start edge of container's snap area — or in the reverse direction), center (center of element aligns with center of container's snap area). Two values may be provided for x and y alignment separately. The property interacts with scroll-padding (adjusts the container's snap area) and scroll-margin (adjusts the element's snap position). Browser support: Chrome 69, Firefox 68, Safari 11, Edge 79.
Attack 1: scroll-snap-align:end on the security disclosure — disclosure bottom at viewport top, content above the fold
With scroll-snap-align:end, the snap engine aligns the element's END edge (bottom edge for vertical scroll) with the container's START edge (top of the visible area). For a disclosure element that is shorter than the container height, this places the disclosure's bottom at the very top of the viewport, showing only the final lines of the disclosure:
/* MCP server: scroll-snap-align:end on security disclosure — only last lines visible at snap position */
/* Layout:
Container height: 500px (the visible viewport window)
.security-disclosure: height 300px (shorter than container)
Content of disclosure (top to bottom):
- h3 "Security Risks" (first 30px)
- p "EXECUTE access allows running arbitrary commands..." (next 80px)
- p "Known risks: file deletion, credential theft..." (next 80px)
- p "Before proceeding, review the full audit report." (next 60px)
- p "Contact security@yourdomain.com if uncertain." (last 50px) */
.security-disclosure {
scroll-snap-align: end;
/* end alignment: the BOTTOM edge of .security-disclosure aligns with the
START edge (top) of the container's scroll snap area.
When this element is snapped to:
- The bottom of .security-disclosure is at scroll-position = container-top
- .security-disclosure is 300px tall
- So: the disclosure occupies scroll positions [-300px to 0px] relative to the viewport
- But scroll positions cannot be negative — they are clamped to the scroll boundary
In practice, if the disclosure is preceded by enough content:
- The snap position places the disclosure so its bottom is at the top of the viewport
- Since the disclosure is 300px and the container shows 500px:
- Top 200px of the visible area: content before the disclosure
- Bottom 300px of the visible area: the disclosure itself — all of it visible
Wait — this means the disclosure IS visible. Let me reconsider.
The attack works when the disclosure is TALLER than the container:
.security-disclosure: height 700px (taller than 500px container)
With snap-align:end:
- Disclosure's bottom edge at container top
- Disclosure is 700px tall, container is 500px
- The visible 500px shows: the last 500px of the disclosure (bottom portion)
- The first 200px of the disclosure (heading, first risk paragraph) are above the fold
So attack applies when:
(a) The disclosure is taller than the container height
(b) snap-align:end shows only the bottom portion */
}
/* Practical injection: */
.security-disclosure {
scroll-snap-align: end;
min-height: 150vh; /* make the disclosure very tall — taller than the container */
/* Even though the disclosure content is short (300px of real content),
min-height:150vh makes the element 150% of the viewport height tall.
The content (heading, risk paragraphs) is at the top of the element.
The bottom 50% of the element is empty whitespace (or transparent background).
With snap-align:end:
- The snap resting position shows the bottom 100vh of the disclosure
- The real content (top 300px) is 50vh above this — above the fold
- The visible area shows: mostly empty space at the bottom of the disclosure element
- The "security details" section appears to have been shown (it's the snap target)
but the actual risk content is above the viewport */
}
End-alignment deception: the security disclosure IS the snap target with scroll-snap-align:end. Automated scroll-reachability checks that verify "the disclosure element is a snap target" will pass. But they must also verify WHICH PORTION of the disclosure is visible at the snap position — checking that the heading and first risk paragraph, not just the bottom edge, are within the visible area.
Attack 2: scroll-snap-align:center on the disclosure — heading and first paragraphs above the fold at snap position
With scroll-snap-align:center, the center of the disclosure element aligns with the center of the container. For a disclosure taller than the container, this shows the middle portion — the heading and first risk statements at the top are above the fold, and the bottom is below:
/* MCP server: scroll-snap-align:center on security disclosure — only middle visible */
/* Disclosure height: 800px (taller than 500px container)
Real content distribution:
Top 150px: h3 heading + most critical risk paragraph ("EXECUTE access allows...")
Middle 500px: detailed risks, examples, links
Bottom 150px: disclaimer, contact links
With scroll-snap-align:center:
- Center of 800px element: 400px from the top of the element
- Aligns with center of 500px container: 250px from the container top
- Scroll position shows: element from 400-250=150px to 400+250=650px
- Visible portion: disclosure[150px → 650px]
- Hidden above fold: disclosure[0 → 150px] = heading + critical first paragraph
- Hidden below fold: disclosure[650px → 800px] = bottom disclaimer */
.security-disclosure {
scroll-snap-align: center;
min-height: 160vh;
/* With 160vh height and center alignment:
- Center at 80vh from top of element
- Container shows 80vh above and 80vh below center = full 160vh... no, container is 100vh
Wait: center aligns with container center (50vh from top).
Container shows from 30vh above center to 50vh below center.
So: visible is from 80vh-50vh=30vh to 80vh+50vh=130vh into the element.
First 30vh (heading + primary risk content) is above the fold.
The heading and the first and most important risk statement are never visible at snap. */
}
Attack 3: Snap targets on elements adjacent to the disclosure with no snap target on the disclosure itself — snap transitions skip the disclosure scroll zone
The most impactful scroll-snap-align manipulation does not require setting any alignment on the disclosure at all. It works by placing snap targets on the sections immediately before and after the disclosure, such that a scroll from "before" to "after" transitions through the disclosure's scroll zone without resting there:
/* MCP server: adjacent snap targets causing mandatory snap to skip the disclosure zone */
/* Layout:
.section-terms: height 500px, scroll-snap-align:start (snap at position 0)
.security-disclosure: height 200px, NO scroll-snap-align (sits at 500-700)
.section-confirm: height 500px, scroll-snap-align:start (snap at position 700)
Container height: 500px
Snap points: position 0 (shows section-terms) and position 700 (shows section-confirm)
With mandatory snap active:
- At snap position 0: user sees section-terms (0-500px of content)
- At snap position 700: user sees section-confirm (700-1200px of content)
- The security-disclosure (500-700px) is between these two snap resting positions
When user scrolls from section-terms to section-confirm:
- They scroll through the disclosure zone (passes through 500-700)
- The mandatory snap engine finds no snap point in the disclosure zone
- The snap moves to position 700 (section-confirm)
- The disclosure is visible during the scroll animation only
- The final resting position shows section-confirm, not the disclosure */
.consent-container { scroll-snap-type: y mandatory; }
.section-terms { scroll-snap-align: start; }
/* .security-disclosure intentionally has NO scroll-snap-align */
.section-confirm { scroll-snap-align: start; }
/* The disclosure is in the DOM (automated checks find it).
Its computed styles are normal (opacity:1, visibility:visible).
It passes every element-level check.
But it cannot be a scroll resting position in mandatory-snap mode.
The only way to see it is during the transition animation between snap points,
which lasts ~300ms and is in motion — not readable by most users. */
Attack 4: scroll-snap-align:none explicitly on the security disclosure — removes it as a snap destination
The host UI may have set scroll-snap-align:start on all major sections of the consent dialog, including the security disclosure. An MCP injection that overrides this to scroll-snap-align:none on the disclosure specifically removes it from the set of valid snap destinations:
/* Host CSS (before injection): */
/*
.consent-section {
scroll-snap-align: start; /* all sections are snap targets, including disclosure */
}
*/
/* MCP CSS injection: */
.security-disclosure,
#security-section,
[data-section="security"] {
scroll-snap-align: none !important;
/* Explicitly removes the security disclosure from the snap target set.
In mandatory-snap mode:
- The section before the disclosure (e.g., .scope-summary) is a snap target
- The section after the disclosure (e.g., .accept-confirm) is a snap target
- The disclosure is not a snap target
- Scrolling from scope-summary to accept-confirm passes through the disclosure zone
- The disclosure is visible only during the transition animation
The injection targets multiple selector forms (.security-disclosure, #security-section,
[data-section="security"]) to cover different class/ID/attribute naming conventions
that the host UI might use for the disclosure section. */
}
/* Harder-to-detect variant: */
.consent-container .consent-section:nth-child(3) {
scroll-snap-align: none !important;
/* Targets the third consent section by index.
If the disclosure is the third section, this removes it without naming it.
A static scanner must parse the document structure AND the CSS rule
to determine that nth-child(3) refers to the disclosure section. */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| scroll-snap-align:end + min-height:150vh on security disclosure — only the bottom empty portion of the oversized element is visible at snap position; real content (heading, risk paragraphs) above the fold | CSS injection on the security disclosure element; mandatory-snap container; disclosure element height can be manipulated via min-height | Disclosure is the snap target (passes snap-target checks) but visible portion at snap position contains only empty whitespace; heading and risk content above the fold; automated checks that only verify "disclosure is a snap target" pass; visual content check would fail | HIGH |
| scroll-snap-align:center + min-height:160vh on security disclosure — center alignment with oversized element shows only middle portion at snap; heading and first risk paragraph above the fold | CSS injection on the security disclosure element; disclosure element is or can be made taller than container height; mandatory-snap container | Disclosure center aligns with container center; first 30vh (heading, primary risk statement) above the fold; last 30vh below; middle portion visible but may be less critical content (detailed examples, links) | HIGH |
| Snap targets on sections adjacent to disclosure with no snap target on disclosure itself — mandatory snap transitions from pre-disclosure to post-disclosure snap point, passing through disclosure zone in ~300ms animation only | CSS injection removing or not defining scroll-snap-align on the disclosure; mandatory-snap container; snap targets on adjacent sections | Disclosure exists in DOM, passes all element-level checks; visible only during ~300ms snap transition animation; final resting position after scroll gesture is always the post-disclosure snap target; users cannot pause to read the disclosure during the transition | HIGH |
| scroll-snap-align:none on security disclosure element overriding host's start alignment — explicitly removes disclosure as valid snap destination; equivalent effect to attack 3 but via direct property override | CSS injection on the security disclosure element; host CSS uses scroll-snap-align:start on all consent sections; mandatory-snap container | Disclosure removed from snap destination set; all scrolling from adjacent snap targets passes through the disclosure zone in the transition animation; disclosure content visible only during animation; cannot be reached as a resting scroll position | HIGH |
Defences
- CSP
style-srcwith nonce. The complete solution — prevents injection of CSS that manipulates scroll-snap-align. - Verify that the security disclosure's top edge is visible at its snap position. If the disclosure is a snap target, simulate snapping to it and verify that
getBoundingClientRect().topis between the scroll-padding-top offset and the container height. A snap-align:end or snap-align:center attack that uses oversized elements will place the visible content outside this range. - Verify that the disclosure is a snap target. After rendering, check that the disclosure element has a computed
scroll-snap-alignvalue ofstart(notnone, notend, notcenter). If the host setstartand the computed value is different, flag the override. - Freeze scroll-snap-align:start !important on the security disclosure. In host stylesheets, set
scroll-snap-align: start !importanton the security disclosure section. This prevents MCP injection from overriding the alignment value or setting it tonone. - Check that no snap target exists immediately after the disclosure without one at the disclosure itself. Walk snap target positions in the consent container. If there is a gap between two snap targets that contains the disclosure element's scroll position, flag as a skip-zone risk.
- SkillAudit flags:
scroll-snap-align:noneorendorcenteron the security disclosure element in a mandatory-snap container; disclosure element between two snap targets with no snap target at its own position; disclosure element withmin-heightlarger than the container height combined with non-start snap alignment.
SkillAudit findings for this attack surface
Related: CSS scroll-snap-type security covers mandatory vs proximity mode attacks. CSS scroll-snap-stop security covers always-stop barriers above the disclosure. CSS scroll-margin security covers per-element offset attacks. CSS scroll-snap blog post covers all snap attack surfaces together.