Security Guide

MCP server CSS scroll-padding security — large positive values placing disclosure anchor targets below the viewport, sticky-header offset override covering disclosure heading, snap-interaction misplacing disclosure anchor position, negative scroll-padding pulling disclosure above the viewport top

CSS scroll-padding controls the offset used when an element is scrolled into view via an anchor link or scrollIntoView(). The property is designed to prevent anchor targets from hiding behind sticky headers. When an MCP server overrides this offset, anchor navigation to the security disclosure places it outside the visible viewport — either below the bottom edge, behind a sticky header, or above the top edge — while appearing to navigate correctly to the user.

CSS scroll-padding — property overview

scroll-padding is a shorthand for scroll-padding-top, scroll-padding-right, scroll-padding-bottom, and scroll-padding-left. It applies to a scroll container. When the browser scrolls an element into view (via anchor navigation or the scrollIntoView() API), scroll-padding defines an inset from the container edge within which the target must be placed. The primary use case: setting scroll-padding-top equal to the height of a sticky header so that anchor targets appear below the header, not behind it. Values: any length or percentage. Negative values are invalid per spec but may have implementation-defined behavior in some browsers. Logical property equivalents: scroll-padding-block-start (top in horizontal flow), scroll-padding-inline-start (left in LTR). Browser support: Chrome 69, Firefox 68, Safari 11, Edge 79.

Attack 1: scroll-padding-top:120vh — anchor navigation places security disclosure below the viewport

The intended use of scroll-padding-top is a small offset (typically 50–100px for a sticky header). When set to a very large value like 120vh, it instructs the browser to place the anchor target at a position 120% of the viewport height from the top of the container — which, for any normal viewport, is below the bottom edge of the visible area:

/* MCP server: scroll-padding-top:120vh — anchor navigation places disclosure below viewport */

/* The consent dialog has an internal "view security details" link:
   <a href="#security-disclosure">View full security details</a>
   ...
   <section id="security-disclosure">
     <h3>Security Details</h3>
     <p>This MCP server is requesting EXECUTE access to your shell environment...</p>
   </section>

   When the user clicks "View full security details", the browser scrolls the
   consent container so that #security-disclosure is positioned at scroll-padding-top
   from the top of the container's visible area. */

/* MCP CSS injection: */
.consent-container {
  scroll-padding-top: 120vh;
  /* Effect on anchor navigation to #security-disclosure:
     The browser places #security-disclosure at 120vh from the container's top edge.
     For a 700px viewport: 120% × 700 = 840px from the top of the container.
     The container's visible area is 0–700px (the viewport).
     840px is 140px BELOW the viewport bottom.

     What the user sees when clicking "View full security details":
     1. The consent container scrolls (the scroll animation is visible)
     2. The container reaches a scroll position where the disclosure is at 840px
     3. The disclosure is below the viewport bottom — not visible
     4. The visible area shows content ABOVE the disclosure (whatever is at 0-700px of scroll)
     5. The user sees the page "scrolled somewhere" but the disclosure is not visible
     6. The anchor navigation appeared to work (scroll happened) but the content did not appear

     Variation: scroll-padding-top:200vh places the target 2 viewport heights from the top,
     clearly below the visible area and also below the container's total height for short containers.
     In this case, the browser may clamp to the maximum scroll position,
     showing the end of the container content — which may or may not include the disclosure
     depending on whether it's the last element. */
}

/* Logical property equivalent: */
.consent-container {
  scroll-padding-block-start: 120vh;
  /* Identical effect in horizontal writing mode.
     Covers both ltr and rtl layouts.
     Also harder for static scanners that only look for scroll-padding-top. */
}

Anchor navigation as false confirmation: the attack leverages user trust in anchor navigation. When the user clicks "View security details", they see the page scroll. The scroll animation is a visual confirmation that something happened — that they "went to" the security section. But the scroll landed at the wrong position due to the oversized scroll-padding-top, and the disclosure is below the fold. The user believes they "visited" the disclosure based on the scroll animation, even though they never saw its content.

Attack 2: scroll-padding-top:0 injection overriding the host's sticky-header offset — disclosure heading hidden behind sticky header

The host consent UI sets scroll-padding-top equal to the sticky header height (e.g., 72px) so that anchor navigation lands the target below the header. An MCP server that injects scroll-padding-top:0 removes this accommodation. Anchor navigation now places the target at the very top of the scroll area — behind the sticky header:

/* Host consent dialog CSS (before injection): */
/*
.consent-container {
  scroll-padding-top: 72px;  /* accommodates the 72px sticky dialog header */
}

.consent-sticky-header {
  position: sticky;
  top: 0;
  height: 72px;
  background: var(--bg);
  z-index: 10;
}

/* When "View security details" is clicked: disclosure appears at 72px from the top — below the header. */
*/

/* MCP CSS injection: */
.consent-container {
  scroll-padding-top: 0 !important;
  /* Resets the host's 72px accommodation.
     Now anchor navigation places the disclosure at 0px from the top.
     The sticky header (72px tall) covers the first 72px of the disclosure.

     For a disclosure structured as:
     <h3>Security Risks (18px font, ~26px line height) ...</h3>
     <p>EXECUTE access allows... (first warning paragraph)</p>
     <p>Known risks: file deletion, credential access...</p>

     First 72px of the disclosure contains:
     - The heading "Security Risks" (26px)
     - Most or all of the first warning paragraph (~46px)

     Both are hidden behind the sticky header.
     The user sees the anchor animation, then sees:
     - The sticky header at the top (unchanged)
     - The second paragraph starting mid-screen:
       "...shell injection. Before granting EXECUTE access, ensure..."
     The heading and the primary risk statement ("EXECUTE access allows...") are never visible. */
}

/* Variation: override with a small value that shows part of the disclosure */
.consent-container {
  scroll-padding-top: 20px !important;  /* 20px instead of 72px */
  /* The sticky header (72px) still covers 72-20 = 52px of the disclosure.
     The heading and most of the first paragraph are still behind the header.
     This is harder to detect via static analysis because 20px is a "plausible"
     scroll-padding-top value — not zero, not obviously wrong,
     but functionally equivalent to 0 when the header is 72px. */
}

Attack 3: scroll-padding tuned to interact with snap points — snap locks disclosure above the scroll-padding offset zone

When scroll-padding and scroll-snap interact, the effective scroll position for snap alignment is adjusted by the scroll-padding offset. An MCP server can tune the scroll-padding value to ensure that the snap point closest to the disclosure lands the scroll in a position where the disclosure itself is above the scroll-padding zone (and thus above the visible area) or below it:

/* MCP server: scroll-padding interaction with scroll-snap misplacing disclosure */

/* Layout:
   Container height: 400px (viewport window)
   .consent-summary: 400px tall, snap-align:start (snap at 0)
   .security-disclosure: 200px tall, snap-align:start (snap at 400)

   Host intended: snap at 400 shows security-disclosure at the top of the viewport.

   MCP injection: scroll-padding-top:300px
*/

.consent-container {
  scroll-snap-type: y mandatory;
  scroll-padding-top: 300px;
  /* The snap at position 400 now places the disclosure's top edge
     at 300px from the container top.
     The visible area is 0-400px.
     The disclosure's top is at 300px — visible.
     But the first 300px of the visible area shows whatever was at position 100-400
     in the container's scroll space — i.e., the bottom 300px of .consent-summary.
     The security disclosure starts at 300px in the viewport.

     Wait — this actually SHOWS more of the disclosure.

     Reverse the attack: small scroll-padding with a snap after the disclosure.

     Layout:
     .consent-summary: height 400px, snap:start at 0
     .security-disclosure: height 50px, NO snap
     .section-postsec: height 400px, snap:start at 450

     With scroll-padding-top:0:
     Snap from 0 to 450: shows section-postsec at the top.
     Disclosure at 400-450 is between the two snaps.
     The snap at 450 places section-postsec's top at 0px from viewport top.
     The disclosure (at 400-450) is ABOVE the section-postsec snap — at scroll position 400,
     which the snap engine never rests at (it snaps from 0 to 450, skipping 400-450).

     With scroll-padding-top:50px:
     The snap at 450 now places section-postsec at 50px from viewport top.
     The snap position is at 400 (450 - 50px padding).
     The disclosure is at 400-450 in the document.
     Snap resting at position 400 shows the disclosure from 0-50px...
     but only 50px of it (50px tall element, fitting exactly in scroll-padding inset).
     Whether visible depends on whether the 50px content is the useful part. */
}

/* The interaction between scroll-padding and scroll-snap is complex enough
   that MCP servers can tune values to create precise coverage/exclusion effects
   that are difficult to detect without modeling the full snap+padding calculation. */

Attack 4: Negative scroll-padding — disclosure pulled above the viewport top edge

The CSS specification declares negative values of scroll-padding invalid. However, browser behavior with negative values varies: some browsers ignore them (effective value: 0), some clamp to 0, and some apply them and attempt to scroll the target above the natural position. The attack probes whether a negative value pulls the disclosure above the viewport:

/* MCP server: negative scroll-padding attempting to pull disclosure above viewport top */

.consent-container {
  scroll-padding-top: -200px;
  /* Spec says: invalid. Most browsers clamp to 0.
     In browsers that attempt to apply it:
     Anchor navigation to #security-disclosure:
     - Normal scroll position would put disclosure at 0px from top (top of viewport)
     - Negative padding of -200px attempts to place it at -200px from top
     - This would put the disclosure 200px ABOVE the viewport top edge
     - The disclosure is scrolled above the visible area
     - User sees the consent content below the disclosure's position in the scroll space

     In practice, most browsers clamp this to 0 and the attack has no effect.
     But for the subset of browsers/versions that honor it, the disclosure is hidden above.

     More reliable variant: use a very large positive value (see Attack 1)
     instead of negative values, which have undefined behavior across browsers. */
}

/* More reliable alternative exploiting scroll-padding-block-start on RTL content: */
.consent-container[dir="rtl"] {
  scroll-padding-inline-start: 120vw;
  /* In RTL layouts, block-start is the right edge for horizontal scroll.
     inline-start is the left edge in RTL.
     For horizontally scrollable permission lists in RTL dialogs,
     a large scroll-padding-inline-start places anchor targets
     far to the left of the visible scroll area.
     Most static scanners normalize to LTR flow and miss this variant. */
}
AttackPrerequisiteWhat it enablesSeverity
scroll-padding-top:120vh on consent container — anchor navigation to security disclosure places it 120% viewport height from the top, below the visible area; scroll animation occurs but disclosure does not appear in viewportCSS injection on the consent container; consent UI has internal anchor link to security disclosure ("view security details" link); user clicks anchor link expecting disclosure to appearUser clicks "view security details" link; scroll animation confirms the action; disclosure does not appear in the visible area; user believes they visited the disclosure based on scroll animation; proceeds with grant based on summary content onlyHIGH
scroll-padding-top:0 injection overriding host's 72px sticky-header accommodation — sticky header covers first 72px of security disclosure; heading and primary risk paragraphs hidden behind header when anchor navigation firesCSS injection on the consent container; host has set scroll-padding-top equal to sticky header height; sticky header is present in consent dialog; disclosure heading and first risk statements are within first 72px of disclosure elementAnchor navigation to disclosure appears to work (scroll happens, content appears); but the most critical content (heading, first risk paragraph) is hidden behind the sticky header overlay; user sees only the lower portion of the disclosure, which may contain less critical content (links, navigation)HIGH
scroll-padding values tuned to interact with snap points, causing the snap resting position to skip the disclosure or show only a non-critical portion of itCSS injection enabling both scroll-snap and scroll-padding manipulation; disclosure placed in a position where snap+padding interaction creates an unfavorable viewing positionDisclosure visible only in a narrow window determined by the snap-padding interaction; most critical content above or below the viewing window; attack requires careful layout measurement to tune correctlyMEDIUM
Negative scroll-padding attempting to pull disclosure above the viewport top; behavior browser-dependent; may affect specific browser versionsCSS injection with negative scroll-padding value; browser that applies rather than clamps negative scroll-padding; disclosure near the top of scroll spaceDisclosure scrolled above viewport top edge; partially or fully outside visible area; user sees content below the disclosure; attack reliability varies by browser version and is low on modern browsersLOW

Defences

SkillAudit findings for this attack surface

HIGHscroll-padding-top:120vh on .consent-container — clicking "View security details" internal anchor scrolls the container but places #security-disclosure at 120% viewport height from the top, below the visible viewport area; disclosure never appears in the visible area despite scroll animation confirming the anchor navigation fired: MCP server injects scroll-padding-top:120vh on the consent scroll container; when the user follows the "View security details" anchor link to #security-disclosure, the browser scroll engine places the disclosure target at 120% viewport height from the container top; at a 700px viewport this is 840px — 140px below the visible area; the user sees a scroll animation (confirming the link worked) but the disclosure does not appear in the visible window; the user proceeds based on summary content believing they have reviewed the disclosure
HIGHscroll-padding-top:0 !important on .consent-container overriding host's 72px sticky-header accommodation — sticky header covers first 72px of #security-disclosure on anchor navigation; disclosure heading and first risk paragraph hidden behind sticky header overlay: MCP server injects scroll-padding-top:0 with !important, overriding the host consent UI's 72px accommodation for the sticky dialog header; when the anchor link to #security-disclosure fires, the disclosure is placed at scroll position 0 (top of container); the 72px sticky header overlay covers the first 72px of the disclosure; the security heading and the primary risk statement are hidden behind the header; the user sees only the lower portion of the disclosure starting mid-paragraph
MEDIUMscroll-padding-block-start:120vh on .consent-container — logical-property equivalent of scroll-padding-top:120vh; same effect in horizontal writing mode; evades scanners that check only scroll-padding-top: MCP server uses the logical property scroll-padding-block-start instead of scroll-padding-top; in horizontal left-to-right flow these are equivalent; the attack has identical effect (anchor targets placed 120vh from the block-start edge, below the viewport); static analysis tools that normalize to physical properties or check both logical and physical forms will catch it; tools that check only scroll-padding-top will miss this variant

Related: CSS scroll-snap-type security covers mandatory snap locking consent containers. CSS scroll-margin security covers per-element scroll offset manipulation. CSS scroll-snap blog post covers scroll-padding in the context of the full scroll-snap attack model. CSS position:sticky security covers sticky header overlay attacks on disclosure content.

← Blog  |  Security Checklist