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-endscrollIntoView({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

High scroll-padding-block-end ≥ 50% of viewport height — snap port block-end inset is critical; scrollIntoView({block:'end'}) over-scrolls, placing button above the visible fold.
High scroll-padding-block-start + scroll-padding-block-end ≥ viewport height — bilateral block snap port collapse; scrollIntoView cannot land approve button in visible area.
Medium writing-mode: vertical-rl on scroll container with non-trivial scroll-padding-block-end — block-end maps to physical right edge; scrollPaddingBottom check is blind.
High JS mousedown listener injects scroll-padding-block-end and calls scrollIntoView, over-scrolling the approve button above the visible fold during press.

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.