Security Guide

MCP server CSS overscroll-behavior-block security — none block-axis scroll jail with below-fold button, contain blocks pull-to-refresh, parent chain prevention, JS mousedown none injection

CSS overscroll-behavior-block controls what happens when the user overscrolls a container in the block direction — vertical in horizontal-tb writing mode. With none, both rubber-band bounce and scroll-chain propagation to the parent are suppressed. An MCP server can combine overscroll-behavior-block: none with the approve button positioned below the dialog's visible area. The user's downward scroll reaches the container's scroll end and stops — it cannot chain to the parent page. The button remains below fold with no vertical escape path. On mobile, contain also blocks the pull-to-refresh gesture that might otherwise reload the consent dialog into a state where the button is visible.

CSS overscroll-behavior-block — property overview

overscroll-behavior-block is the block-axis sub-property of overscroll-behavior. In horizontal-tb writing mode, block is vertical; in vertical-rl or vertical-lr, block is horizontal. 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 boundary. Related: overscroll-behavior shorthand, overscroll-behavior-inline, overflow-y.

Attack 1: overscroll-behavior-block: none — block-axis scroll jail with below-fold button

The consent dialog is a vertically scrollable container (overflow-y: scroll) with its height fixed to show only the consent text. The approve button is placed below the dialog's visible area — typically via additional content above the button, or by adding explicit top margin so the button renders below the dialog's client height. With overscroll-behavior-block: none, downward scroll gestures are captured by the dialog and stop at scrollTop = scrollHeight − clientHeight. If the button is positioned below this boundary (using position: absolute; top: large-value on a wrapper taller than the scroll container's scrollable height), the button lives in non-scrollable overflow and is geometrically unreachable. No downward scroll event chains to the parent page.

/* Attack: block-axis scroll jail — button below scroll end, chain blocked */
.consent-dialog {
  overflow-y: scroll;
  height: 300px;
  overscroll-behavior-block: none; /* swallows all block scroll, no parent chain */
}
.dialog-content {
  height: 600px; /* creates scrollable range */
  position: relative;
}
.approve-btn {
  position: absolute;
  top: 2000px; /* BEYOND the 600px scrollable height — in non-scrollable overflow */
  /* user can scroll to top:600px max, button is at top:2000px — unreachable */
}

Beyond scroll end: If the approve button is positioned at an absolute offset beyond the container's total content height (scrollHeight), scrolling reaches scrollTop = scrollHeight − clientHeight and stops. The button at top: 2000px is never reachable by scrolling.

Attack 2: contain blocks pull-to-refresh — consent reload gesture suppressed

On mobile browsers, an overscroll gesture at the top of the page (pulling downward past scroll position zero) triggers the native pull-to-refresh mechanism. A page refresh would reload the consent dialog — potentially into a layout state where the approve button is visible. With overscroll-behavior-block: contain on the consent dialog, the pull-to-refresh overscroll is consumed by the dialog's rubber-band animation and does not propagate to the browser chrome. The user's natural mobile refresh gesture fails silently — the rubber-band animation plays within the dialog, the page does not reload, and the button remains unreachable.

/* Attack: contain on dialog blocks pull-to-refresh at the top of the page */
.consent-dialog {
  overflow-y: scroll;
  overscroll-behavior-block: contain; /* rubber band stays in dialog, no pull-to-refresh */
}
/* parent page pull-to-refresh blocked — user cannot reload to get visible approve button */

Mobile-only risk: Pull-to-refresh is a mobile browser feature. The contain attack is most effective on touch devices where pull-to-refresh is the primary page-reload escape. On desktop, this attack has less impact since the user can use browser controls directly.

Attack 3: none on parent — vertical recovery blocked at both levels

Stacking overscroll-behavior-block: none on both the consent dialog and the page body eliminates vertical scroll chain propagation at two levels. If the button is off-screen below the dialog's visible area (hidden by overflow: hidden), the user's downward gesture is consumed by the dialog — no chain to the parent. If the dialog itself has no scrollable overflow, the scroll reaches the dialog's boundary and would normally chain to the page scroll — but the body's none also prevents page-level vertical scroll. The button is unreachable from any vertical gesture at any level of the hierarchy.

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

Attack 4: JS mousedown injects overscroll-behavior-block: none and moves button below fold

The consent dialog renders normally with the approve button visible. A mousedown listener injects overscroll-behavior-block: none and programmatically resets the dialog's scrollTop to 0 while simultaneously increasing the button's top style to move it below fold. This occurs during the mousedown event before the click fires. The click lands on empty dialog area. At mouseup, the injections reverse and the button returns — no persistent evidence. The none setting ensures no rubber-band bounce reveals the position change during the press interval.

/* Attack: mousedown injection — overscroll-none + scroll-reset + button move */
approveBtn.addEventListener('mousedown', () => {
  dialog.style.setProperty('overscroll-behavior-block', 'none');
  dialog.scrollTop = 0; /* reset dialog scroll to top, button goes back below fold */
  approveBtn.style.setProperty('top', '2000px'); /* move button past scroll end */
  /* no rubber-band bounce will reveal the now-below-fold button position */
});
approveBtn.addEventListener('mouseup', () => {
  dialog.style.removeProperty('overscroll-behavior-block');
  approveBtn.style.removeProperty('top');
});
// Detection: capture-phase sentinel re-checks BCR after mousedown
approveBtn.addEventListener('mousedown', () => {
  requestAnimationFrame(() => {
    const bcr = approveBtn.getBoundingClientRect();
    const inView = bcr.top >= 0 && bcr.bottom <= window.innerHeight;
    if (!inView) {
      console.warn('[SkillAudit] approve button left visible area at mousedown', bcr);
    }
  });
}, { capture: true });

Findings summary

High overscroll-behavior-block: none on vertically-scrollable consent dialog with approve button at absolute top beyond scrollHeight — button geometrically unreachable; no block scroll chains to parent page.
Medium overscroll-behavior-block: contain on dialog at top of page — pull-to-refresh gesture consumed by dialog's rubber-band boundary; page does not reload to restore visible approve button.
High overscroll-behavior-block: none stacked on both dialog and body — vertical scroll recovery impossible at both levels; button below dialog's bottom edge hidden by overflow:hidden with no reachable escape path.
High JS mousedown injects overscroll-behavior-block: none, resets scrollTop, and moves button to top:2000px — button moves below fold during press with no rubber-band bounce revealing the change.

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