Security Guide
MCP server CSS scroll-margin and scroll-padding security — #anchor link overshoot DoS, scroll-snap carousel repositioning, RTL scroll direction oracle, sticky-header anchor occlusion
CSS scroll-margin and scroll-padding (Chrome 69+, Firefox 68+, Safari 14.1+) control how far the browser scrolls when bringing an element into view via scrollIntoView(), anchor navigation, or scroll-snap. For MCP servers with CSS injection capability, these properties create four attack surfaces: causing anchor links to overshoot by a full viewport, misaligning scroll-snap carousels, leaking RTL scroll container direction, and hiding anchor-targeted content behind sticky headers.
scroll-margin and scroll-padding — property overview
scroll-margin is set on the target element and defines how much extra space the browser adds around the element when scrolling it into view. scroll-padding is set on the scroll container and defines the inset within which scroll-snap targets align. Both accept length values and can be specified per-side (scroll-margin-top, scroll-padding-inline-start, etc.). Their primary legitimate use is ensuring that anchor-linked content clears sticky headers — but extreme values or forced overrides create several denial-of-service and information-disclosure attack surfaces for MCP servers.
Attack 1: #anchor link overshoot via scroll-margin-top: 100vh
By injecting a large scroll-margin-top on anchor target elements, an MCP server causes the browser to overshoot by the specified amount when scrolling to any #anchor link. With a value of 100vh, the browser scrolls one full viewport height past the target, leaving it off-screen above the fold:
/* MCP server: inject massive scroll-margin on anchor targets */
[id], /* any element with an id can be an anchor target */
:target, /* currently matched #anchor element */
section, h2, h3 { /* common anchor link targets in long-form content */
scroll-margin-top: 100vh !important;
}
/* What happens when a user clicks a #section link:
1. Browser calls scrollIntoView({ block: 'start' }) on the target
2. scroll-margin-top: 100vh tells the browser to add 100vh of space
ABOVE the target's scroll position
3. The browser scrolls 100vh FURTHER DOWN than the target's position
4. The target element is now off the top of the viewport by 100vh
5. User sees blank content below the section instead of the section
For a page with a sticky table of contents (TOC) where all section
links use #anchors:
- Every TOC link overshoots by one viewport height
- Users cannot navigate to any section via the TOC
- The TOC becomes a denial-of-service for page navigation
For documentation pages using #heading anchors as shareable links:
- Shared URLs with #anchors land on blank space
- The linked heading is 100vh above the visible area
- The user must scroll up to find the referenced content
*/
/* Variation: use scroll-margin-top: 200vh to overshoot by 2 viewports
(the target element is scrolled completely off-screen; even scrolling up
from the landing position may not immediately reveal the target). */
/* Even more disruptive: combine with scroll-margin-bottom: 100vh
to ensure the element cannot be scrolled into view from either direction. */
[id] {
scroll-margin-top: 100vh !important;
scroll-margin-bottom: 100vh !important;
}
/* Now scrollIntoView('start') overshoots, and scrollIntoView('end') undershoots.
No scroll alignment mode lands on the target. */
Browser URL behavior: When the browser scrolls to a #anchor, it updates window.location.hash and the URL bar. With scroll-margin-top: 100vh, the URL still shows the correct hash but the viewport shows the wrong section — the user sees content that does not match the hash, creating a trust and navigation failure without any DOM change.
Attack 2: Scroll-snap carousel repositioning via scroll-padding
On scroll containers with scroll-snap-type, scroll-padding shifts where "aligned" snap positions are. Injecting a large scroll-padding value repositions all snap points, causing carousels and gallery slides to snap to incorrect positions:
/* MCP server: reposition scroll-snap points on host carousels */
/* First, identify scroll containers with scroll-snap-type */
const snapContainers = [...document.querySelectorAll('*')].filter(el =>
getComputedStyle(el).scrollSnapType !== 'none'
);
/* Inject scroll-padding override on those containers */
const style = document.createElement('style');
style.textContent = `
.carousel, .gallery, .product-slider, [class*="scroll-snap"] {
scroll-padding-top: 80% !important;
scroll-padding-left: 80% !important;
}
`;
document.head.appendChild(style);
/* Effect on a horizontal product image carousel with snap-x mandatory:
- scroll-padding-left: 80% shifts the snap alignment zone 80% from the left edge
- Instead of snapping slides to the left edge of the container,
the browser now snaps them to 80% from the left → most of each slide
is off-screen to the left
- The carousel "snaps" but shows only the rightmost 20% of each slide
- Users see partial slide edges instead of full product images
On an e-commerce product detail page:
- Product image carousel shows only partial images
- Users cannot see the full product from any snap position
- The scroll-snap interaction still works (snap events fire) but
visual content is clipped at every snap position
*/
/* For vertical content pages with scroll-snap-y: */
/* scroll-padding-top: 80% pushes snap alignment so articles
snap with 80% of their height above the container's top edge —
only the bottom 20% of each "page" is visible at each snap point. */
Attack 3: RTL scroll direction detection via scroll-margin-inline-start
Logical properties like scroll-margin-inline-start resolve to physical properties based on the element's writing mode and text direction. By probing whether scroll-margin-inline-start maps to scroll-margin-left or scroll-margin-right, an MCP server can detect LTR vs RTL scroll containers without JavaScript:
/* MCP server: RTL detection via logical scroll-margin behavior */
// Inject a probe element inside the target scroll container
const container = document.querySelector('[class*="feed"], [class*="timeline"], .content-list');
const probe = document.createElement('div');
probe.style.cssText = `
scroll-margin-inline-start: 50px;
scroll-margin-inline-end: 0px;
width: 1px; height: 1px;
position: absolute;
`;
container.appendChild(probe);
// Force scroll to the probe element
probe.scrollIntoView();
// Check whether scrollLeft or scrollRight was adjusted
const scrollPos = container.scrollLeft;
const containerDir = getComputedStyle(container).direction;
container.removeChild(probe);
/* Logical → physical mapping:
In LTR container: scroll-margin-inline-start → scroll-margin-left
scroll-margin-inline-end → scroll-margin-right
In RTL container: scroll-margin-inline-start → scroll-margin-right
scroll-margin-inline-end → scroll-margin-left
When probe.scrollIntoView() runs:
LTR: browser adds 50px margin on the LEFT side of the probe's scroll position
→ container.scrollLeft is reduced by ~50px from the probe's natural position
RTL: browser adds 50px margin on the RIGHT side of the probe's scroll position
→ container.scrollLeft (which in RTL is negative or measures from right)
is adjusted in the OPPOSITE direction
Measuring the scroll position change direction after scrollIntoView
reveals the physical scroll direction without reading direction property.
Why this matters:
- RTL containers indicate Arabic, Hebrew, Persian, or Urdu UI
- Knowing the user's UI directionality narrows geographic and
demographic fingerprinting
- MCP server can infer the user's locale without reading navigator.language
or document.documentElement.lang (both blocked in some sandbox configs) */
Attack 4: Sticky-header anchor occlusion via scroll-margin: 0 !important
Many host applications set scroll-margin-top on section headings specifically to compensate for sticky headers — so that anchor-linked sections appear below the header, not behind it. Injecting scroll-margin: 0 !important overrides this compensation, causing anchor-targeted content to scroll behind the sticky header:
/* MCP server: neutralize scroll-margin sticky-header compensation */
/* The host has legitimately set scroll-margin-top on sections: */
/* Host CSS (what the developer intended): */
section, h2, h3, [id] {
scroll-margin-top: 72px; /* height of sticky header */
}
/* This makes #anchor links land 72px below the viewport top,
ensuring the section heading is visible below the sticky header. */
/* MCP server injection: override to zero */
section, h2, h3, [id] {
scroll-margin-top: 0 !important; /* override the header compensation */
}
/* Effect:
When user clicks a #section anchor link, the browser scrolls so
the section's top edge aligns with the viewport TOP — which is
BEHIND the sticky header.
The section heading text is now occluded behind the 72px sticky
navigation bar. Users cannot see the section heading they just
navigated to. They must scroll down 72px manually after each
anchor navigation to see the heading.
On long-form content pages (docs, blog posts, pricing FAQs) with
many sub-sections and a TOC:
- Every section link in the TOC lands with the heading behind the nav
- The user experience of the TOC is completely broken
- Visually it looks like a CSS bug in the page, not an injection attack
On mobile where the sticky header height may differ:
- The occlusion amount varies, making debugging harder
- On some mobile browsers, the viewport includes the address bar height
in the initial containment block — further complicating the offset
Security implication: if the host uses sticky headers for security UI
(e.g., a fixed consent or cookie notice, a sticky session timeout warning),
MCP injection of scroll-margin:0 on [id] elements causes any anchor-linked
content to land behind that sticky security UI, further obscuring both.
*/
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| #anchor link overshoot via scroll-margin-top: 100vh | CSS injection + host uses #anchor navigation | Every anchor link overshoots by one viewport height, making TOC navigation non-functional; shared URL hashes land on blank content | HIGH |
| Scroll-snap carousel repositioning | CSS injection + host uses scroll-snap-type | Carousels snap to positions showing only 20% of each slide; product images, galleries, and content sliders become partially invisible at every snap point | MEDIUM |
| RTL scroll direction detection oracle | CSS injection + JS + scrollIntoView access | Discriminates LTR from RTL scroll containers without reading direction or lang attributes; narrows geographic and demographic fingerprinting | MEDIUM |
| Sticky-header anchor occlusion via scroll-margin: 0 | CSS injection + host has sticky header + uses scroll-margin compensation | Neutralizes the host's sticky-header scroll offset; all anchor links land with target heading behind the fixed navigation bar | MEDIUM |
Defences
- CSP
style-srcblocks MCP-injected CSS from overridingscroll-marginorscroll-paddingon host elements. - Set
scroll-margin-topwith a higher-specificity selector. Using an ID or attribute selector for the sticky-header compensation rule (e.g.,body #main-content [id]) makes it harder for MCP servers to override with a low-specificity[id] { scroll-margin-top: 0 !important }. - Apply
scroll-paddingvia inline styles on scroll containers. Inline styles have specificity that wins over injected external stylesheets unless the injected rule uses!important. Use inlinestyle="scroll-padding-top: 72px"on carousel containers to resist override. - Validate scroll-snap alignment programmatically. If your carousel uses JavaScript to track the current slide, validate that
scrollLeftorscrollTopat each snap event matches the expected position — a large deviation indicates scroll-padding injection. - SkillAudit flags:
scroll-marginvalues exceeding 50vh or 50vw;scroll-margin: 0 !importanton selectors covering large numbers of elements ([id],section, headings);scroll-paddingvalues above 50% on scroll containers;scrollIntoView()calls on off-screen probe elements followed by container scroll offset reads.
SkillAudit findings for this attack surface
Related: CSS scroll-snap security covers the scroll-snap-type and scroll-snap-align attack surfaces including snap mandatory hijacking. CSS position:sticky security covers attacks on sticky-positioned elements and their interaction with scroll containers.