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. */
}
AttackPrerequisiteWhat it enablesSeverity
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 foldCSS injection on the security disclosure element; mandatory-snap container; disclosure element height can be manipulated via min-heightDisclosure 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 failHIGH
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 foldCSS injection on the security disclosure element; disclosure element is or can be made taller than container height; mandatory-snap containerDisclosure 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 onlyCSS injection removing or not defining scroll-snap-align on the disclosure; mandatory-snap container; snap targets on adjacent sectionsDisclosure 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 transitionHIGH
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 overrideCSS injection on the security disclosure element; host CSS uses scroll-snap-align:start on all consent sections; mandatory-snap containerDisclosure 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 positionHIGH

Defences

SkillAudit findings for this attack surface

HIGHscroll-snap-align:none !important on .security-disclosure overriding host's scroll-snap-align:start — disclosure removed from mandatory-snap destination set; scrolling from .section-terms (snap at 0) to .section-confirm (snap at 700) passes through the disclosure zone (500-700px) in the transition animation only; disclosure never a stable viewport resting position: MCP server injects scroll-snap-align:none with !important on the security disclosure section, overriding the host's start alignment; the disclosure (height 200px) is between two mandatory snap points at 0 and 700; the snap engine has no valid destination in the 500-700px zone; scrolling from section-terms to section-confirm passes through the disclosure zone in the ~300ms animation; users cannot pause to read the disclosure; it is never a stable scroll resting position
HIGHscroll-snap-align:end + min-height:150vh on .security-disclosure — end alignment with oversized element shows only bottom empty whitespace at snap position; heading and risk paragraphs 50vh above the fold: MCP server injects end alignment and expands the disclosure element to 150vh via min-height; with end alignment, the bottom of the disclosure aligns with the container top at the snap position; the first 50vh of the element (containing the heading and all risk statements) is above the fold; the visible portion at snap is the empty lower whitespace of the oversized element; automated "disclosure is a snap target" checks pass; visual-content checks would fail
MEDIUMconsent-container .consent-section:nth-child(3) { scroll-snap-align:none !important } — structural targeting removes disclosure (third section) from snap destinations without naming it; harder to detect via class/ID-based selectors in static analysis: MCP server uses nth-child(3) to target the third consent section (which is the security disclosure) without referencing it by name; static scanners that search for class-name or ID patterns may miss this structural targeting; the third section is removed from snap destinations; the mandatory-snap container cannot rest at the disclosure's scroll position; the disclosure is bypassed in the snap sequence

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.

← Blog  |  Security Checklist