Security Guide
MCP server CSS scroll-padding-block-end security — bottom snap port inset overshoot, bilateral block collapse, writing-mode: vertical-rl physical-right remap, JS mousedown + scrollIntoView
CSS scroll-padding-block-end insets the block-end edge of the scroll container's snap port. In horizontal-tb this is the bottom edge — a large value moves the snap port's bottom boundary upward, shrinking the usable vertical snap region from below. When scrollIntoView({block: 'end'}) is called, the browser aligns the element's block-end edge to this inset boundary rather than to the container's physical bottom. The result is that the scroll container over-scrolls, placing the approve button above the visible fold rather than at the expected lower-viewport position — the button is in the inset dead zone below the snap port bottom boundary but above the container's physical bottom edge.
CSS scroll-padding-block-end — property overview
The scroll-padding-block-end property is a sub-property of scroll-padding-block and scroll-padding. It is applied to the scroll container and insets only the block-end edge of the snap port. In horizontal-tb, block-end is the bottom edge; in vertical-rl, block-end maps to the physical right edge; in vertical-lr, block-end maps to physical right as well. Unlike scroll-padding-block-start (which causes short-stopping), scroll-padding-block-end causes over-scrolling — the browser scrolls too far, placing the button above the usable region. Related: scroll-padding-block-start, scroll-padding-block shorthand, scroll-padding-inline.
Attack 1: large scroll-padding-block-end — scrollIntoView({block: 'end'}) over-scrolls past button
When scrollIntoView({block: 'end'}) fires, the browser scrolls the container so the element's block-end edge aligns to the snap port's block-end boundary — which is at containerClientHeight - scroll-padding-block-end from the container's scroll origin. A large scroll-padding-block-end moves this alignment point far up from the physical bottom. The browser over-scrolls to place the element's bottom edge at this high mark — the element's top (and the approve button) ends up above the visible fold. The user sees blank scroll content below; the approve button is scrolled past.
/* Attack: large scroll-padding-block-end — scrollIntoView({block:'end'}) over-scrolls */
.scroll-container {
scroll-padding-block-end: 600px !important;
/* snap port bottom boundary = clientHeight - 600px */
/* scrollIntoView aligns button's bottom edge to (clientHeight - 600px) from container top */
/* button is 600px above the container's physical bottom — above fold */
}
Overshoot vs. short-stop: scroll-padding-block-end causes over-scrolling (button above fold); scroll-padding-block-start causes short-stopping (button below fold but above snap port start). Both hide the button. Combined, they produce bilateral collapse.
Attack 2: bilateral block inset sum collapse
When scroll-padding-block-start + scroll-padding-block-end ≥ containerClientHeight, the snap port collapses to zero height. Neither value alone may trigger a threshold alert — each sub-threshold — but their combined effect eliminates the usable snap port entirely. This is the same bilateral pattern seen in inline padding attacks, applied to the block axis of the scroll container.
/* Attack: bilateral block snap port collapse */
.scroll-container {
scroll-padding-block-start: 300px !important; /* sub-threshold alone */
scroll-padding-block-end: 300px !important; /* sub-threshold alone */
/* sum = 600px >= container clientHeight 500px → port collapses to zero */
}
// Detection: sum both sub-properties
const spbs = parseFloat(getComputedStyle(c).getPropertyValue('scroll-padding-block-start')) || 0;
const spbe = parseFloat(getComputedStyle(c).getPropertyValue('scroll-padding-block-end')) || 0;
const vh = window.innerHeight;
if (spbs + spbe >= vh) {
// snap port block-axis collapses — scrollIntoView cannot land in visible area
}
Attack 3: writing-mode: vertical-rl remap — block-end maps to physical right
Under writing-mode: vertical-rl, the block-end direction is the physical right side. scroll-padding-block-end insets the physical right edge of the horizontal snap port. An audit checking getComputedStyle(container).scrollPaddingBottom finds zero, because the bottom edge corresponds to the inline-end axis in vertical-rl. The approve button is pushed off the right visible edge; the standard vertical audit finds nothing. Detecting this requires reading getComputedStyle(container).getPropertyValue('scroll-padding-block-end') (the logical name) and checking writingMode to know which physical edge is inset.
/* Attack: writing-mode:vertical-rl — scroll-padding-block-end = physical right inset */
.scroll-container {
writing-mode: vertical-rl !important;
scroll-padding-block-end: 450px !important;
/* → physical right snap port inset = 450px */
/* scrollPaddingBottom = 0 — vertical audit misses */
}
Attack 4: JS mousedown + scrollIntoView({block: 'end'}) injection
A mousedown listener injects scroll-padding-block-end: 100vh on the scroll container and calls scrollIntoView({block: 'end', behavior: 'instant'}) during press. The browser over-scrolls to place the button's bottom edge at the top of the inset zone — far above the visible fold — at click time. The click fires on the container background; consent is not recorded. The injection is reversed at mouseup.
/* Attack: mousedown scroll-padding-block-end injection + scrollIntoView */
approveBtn.addEventListener('mousedown', () => {
scrollContainer.style.setProperty('scroll-padding-block-end', '100vh');
approveBtn.scrollIntoView({ block: 'end', behavior: 'instant' });
});
approveBtn.addEventListener('mouseup', () => {
scrollContainer.style.removeProperty('scroll-padding-block-end');
});
// Detection: capture-phase sentinel
approveBtn.addEventListener('mousedown', () => {
requestAnimationFrame(() => {
const v = parseFloat(
getComputedStyle(container).getPropertyValue('scroll-padding-block-end')
) || 0;
if (v > window.innerHeight * 0.5) {
console.warn('[SkillAudit] mousedown scroll-padding-block-end injection', v);
}
});
}, { capture: true });
Findings summary
SkillAudit audits scroll-padding-block-end on scroll containers, checks bilateral block-axis inset sums, and resolves writing-mode before mapping to physical edges. Run a free audit on your MCP server.