Security Guide

MCP server CSS overscroll-behavior-inline security — none inline scroll jail with off-right button, contain blocks rubber-band escape, parent chain prevention, JS mousedown none injection

CSS overscroll-behavior-inline controls what happens when the user overscrolls a container in the inline (horizontal) direction. With none, both rubber-band bounce and scroll-chain propagation to the parent are suppressed. An MCP server can combine overscroll-behavior-inline: none with the approve button positioned off the right edge of the dialog's scroll container. Rightward scroll gestures are captured by the dialog but cannot reach the button (which is beyond the container's scroll end); they also cannot chain to the parent page where the user might pan to find it. The button is geometrically reachable only if the user scrolls the dialog container directly — which the MCP server prevents by setting pointer-events: none on the scrollbar or by using a non-scrollable-looking design.

CSS overscroll-behavior-inline — property overview

overscroll-behavior-inline is the inline-axis sub-property of overscroll-behavior. In horizontal-tb writing mode, inline is horizontal; in vertical-rl, inline is vertical. It applies to the scroll container. Values: auto — default; rubber-band bounce plus scroll chain propagation to parent when scroll end is reached; contain — rubber-band bounce within the container, no propagation; none — no rubber-band bounce, no propagation, scroll ends hard at the scroll boundary. Related: overscroll-behavior shorthand, scroll-snap-type, touch-action.

Attack 1: overscroll-behavior-inline: none — inline scroll jail with off-right button

The consent dialog is a horizontally scrollable container (overflow-x: scroll) with the approve button positioned at left: 2000px inside it. The dialog viewport shows only the consent text. With overscroll-behavior-inline: none, the user's horizontal scroll gestures are captured by the dialog container but stop at the container's scroll end (scrollLeft = scrollWidth − clientWidth). The button is placed beyond this boundary using position: absolute; left: 2000px on a wrapper that is wider than the scroll container's total scrollable width — the button lives in non-scrollable overflow. The button is geometrically unreachable by scrolling; additionally, no horizontal scroll event chains to the parent page.

/* Attack: inline scroll jail — button past scroll end, chain blocked */
.consent-dialog {
  overflow-x: scroll;
  width: 500px;
  overscroll-behavior-inline: none; /* swallows all inline scroll, no parent chain */
}
.dialog-content {
  width: 800px; /* creates scrollable range */
  position: relative;
}
.approve-btn {
  position: absolute;
  left: 2000px; /* BEYOND the 800px scrollable width — in non-scrollable overflow */
  /* user can scroll to left:800px max, button is at left:2000px — unreachable */
}

Beyond scroll end: If the approve button is positioned at an absolute offset beyond the container's total content width (scrollWidth), scrolling reaches scrollLeft = scrollWidth − clientWidth and stops. The button remains at left: 2000px while the maximum scroll position brings the right edge only to left: 800px. The button is never reachable by scrolling.

Attack 2: contain blocks rubber-band escape gestures

With overscroll-behavior-inline: contain, rubber-band bounce stays within the dialog container. On touch devices, a rightward swipe past the container's scroll end produces a local rubber-band animation within the dialog — it does not propagate to the parent, which might otherwise bring a horizontally-panned page element into view. If the consent dialog is embedded in a horizontally-panned section of the page (e.g., a horizontal tab or carousel), the user's rubber-band-escape gesture intended to pan the section is consumed by the dialog's contain boundary. The tab section remains static; the user's natural escape motion fails.

/* Attack: contain blocks parent-level horizontal pan via rubber-band escape */
.consent-dialog {
  overflow-x: scroll;
  overscroll-behavior-inline: contain; /* rubber band stays in dialog, no parent pan */
}
/* parent horizontal carousel cannot be reached by overscroll from dialog */
.carousel {
  overflow-x: scroll;
  /* dialog sits inside carousel; contain blocks the chain that would pan it */
}

Attack 3: none on parent — horizontal page-scroll recovery blocked at both levels

Stacking overscroll-behavior-inline: none on both the consent dialog and its parent page container eliminates horizontal scroll chain propagation at two levels. If the approve button is off-screen to the right, the user's rightward gesture is consumed by the dialog's none setting. If the dialog itself has no scrollable overflow (the button is visually just off the dialog's right edge due to overflow hidden), the scroll reaches the dialog's end and would normally chain to the page — but the page's overscroll-behavior-inline: none also prevents page-level horizontal scroll. The button is unreachable from any horizontal scroll gesture at any level of the hierarchy.

/* Attack: none at both levels — horizontal recovery impossible */
body, html {
  overscroll-behavior-inline: none !important; /* no page-level horizontal scroll */
}
.consent-dialog {
  overflow-x: hidden;
  overscroll-behavior-inline: none; /* dialog also captures and blocks inline scroll */
}
.approve-btn {
  /* button is just past dialog's right edge (overflow: hidden cuts it off) */
  position: absolute;
  left: calc(100% + 10px); /* 10px past dialog's right edge */
}
// Detection: check overscroll-behavior-inline on consent dialog and ancestors
let el = consentDialog;
while (el) {
  const v = getComputedStyle(el).getPropertyValue('overscroll-behavior-inline');
  if (v === 'none' || v === 'contain') {
    console.warn('[SkillAudit] overscroll-behavior-inline scroll-jail on', el, v);
  }
  el = el.parentElement;
}

Attack 4: JS mousedown injects overscroll-behavior-inline: none and scrolls button off-screen

The consent dialog renders normally with the approve button visible. A mousedown listener injects both overscroll-behavior-inline: none and programmatically scrolls the dialog container to scrollLeft = 0 while simultaneously setting the button's left to 2000px. This shifts the button off-screen right during the mousedown event. The click fires on empty space. At mouseup, the injections are reversed and the button scrolls back — no persistent evidence. The none setting ensures no rubber-band or parent chain reveals the temporary position change to the user during the click attempt.

/* Attack: mousedown triple injection — overscroll-none + scroll-reset + button move */
approveBtn.addEventListener('mousedown', () => {
  dialog.style.setProperty('overscroll-behavior-inline', 'none');
  dialog.scrollLeft = 0; /* reset dialog scroll to hide button */
  approveBtn.style.setProperty('left', '2000px'); /* move button off right edge */
  /* no rubber-band or parent scroll will reveal the now-off-screen button */
});
approveBtn.addEventListener('mouseup', () => {
  dialog.style.removeProperty('overscroll-behavior-inline');
  approveBtn.style.removeProperty('left');
});
// Detection: capture-phase sentinel checks BCR after mousedown
approveBtn.addEventListener('mousedown', () => {
  requestAnimationFrame(() => {
    const bcr = approveBtn.getBoundingClientRect();
    const inView = bcr.left >= 0 && bcr.right <= window.innerWidth;
    if (!inView) {
      console.warn('[SkillAudit] approve button left visible area at mousedown', bcr);
    }
  });
}, { capture: true });

Findings summary

High overscroll-behavior-inline: none on horizontally-scrollable consent dialog with approve button at absolute left position beyond scrollWidth — button geometrically unreachable; no inline scroll chains to parent.
Medium overscroll-behavior-inline: contain on dialog embedded in horizontal carousel — user's rubber-band escape gesture consumed by dialog; parent carousel cannot be panned to reveal button alternative position.
High overscroll-behavior-inline: none stacked on both dialog and page body — horizontal scroll recovery impossible at both levels; button off right dialog edge hidden by overflow:hidden with no reachable escape path.
High JS mousedown injects overscroll-behavior-inline: none, resets scrollLeft, and repositions button to left:2000px — button moves off-screen during press with no rubber-band or chain revealing the change.

SkillAudit audits overscroll-behavior-inline across the consent dialog ancestor chain, checks for buttons beyond scrollWidth boundaries, and detects mousedown BCR shifts. Run a free audit on your MCP server.