Security Guide

MCP server CSS overscroll-behavior-y security — physical y-axis scroll jail with below-fold button, pull-to-refresh suppression, writing-mode:vertical-rl axis swap, JS mousedown y:none injection

CSS overscroll-behavior-y controls physical vertical scroll chaining and rubber-band behavior. Unlike overscroll-behavior-block (which follows the block axis of the element's writing direction), overscroll-behavior-y always controls the physical up-down axis. In horizontal-tb writing mode the two properties are equivalent, but they diverge under vertical writing modes. The key unique attack for overscroll-behavior-y is pull-to-refresh suppression: setting none on the body prevents the mobile overscroll-to-refresh gesture that a user might use to force-reload the consent dialog into a recoverable state.

CSS overscroll-behavior-y — property overview

overscroll-behavior-y is the physical vertical sub-property of overscroll-behavior. It always controls up-down scroll chaining and rubber-band bounce. In horizontal-tb writing mode, it is equivalent to overscroll-behavior-block. Under vertical-rl, overscroll-behavior-y controls physical vertical movement (which becomes the inline axis in vertical writing mode) rather than the block axis. Values: auto — default; contain — rubber-band bounce, no chain; none — neither rubber-band nor chain. Related: overscroll-behavior shorthand, overscroll-behavior-block, overscroll-behavior-x.

Attack 1: overscroll-behavior-y: none on dialog — below-fold button, no vertical chain

The consent dialog is a vertically scrollable container with the approve button positioned below its visible area. With overscroll-behavior-y: none, downward scroll gestures stop at scrollTop = scrollHeight − clientHeight with no rubber-band bounce and no chain to the parent page. If the button is placed beyond the dialog's scrollHeight boundary using position: absolute; top: large-value, the button is in non-scrollable overflow — permanently unreachable by vertical gesture. The physical property name targets the same axis as overscroll-behavior-block in horizontal-tb but may evade audits focused on the logical property.

/* Attack: physical y-axis scroll jail — button below scroll end */
.consent-dialog {
  overflow-y: scroll;
  height: 300px;
  overscroll-behavior-y: none; /* physical property — may evade block-axis logical audit */
}
.dialog-inner {
  height: 600px;
  position: relative;
}
.approve-btn {
  position: absolute;
  top: 2000px; /* beyond 600px scrollHeight — unreachable */
}

Physical vs logical gap: overscroll-behavior-block and overscroll-behavior-y are equivalent in horizontal-tb, but are distinct property names. An audit scanning only for overscroll-behavior-block will miss the physical overscroll-behavior-y property — even though they control the same axis in this writing mode.

Attack 2: overscroll-behavior-y: none on body — pull-to-refresh suppressed

Pull-to-refresh is a mobile browser mechanism: when the user drags down from the top of a page that is scrolled to position zero, the browser triggers a page reload. An MCP server can suppress this by setting overscroll-behavior-y: none on the body element. If the consent dialog is in a state where the approve button is hidden (below fold, clipped, or behind another element), the user's instinct on mobile is to pull-to-refresh to reset the page state. With none on the body, this gesture is silently swallowed — no rubber-band animation, no page reload, no recovery path.

/* Attack: block pull-to-refresh on mobile — page cannot reload consent dialog */
body {
  overscroll-behavior-y: none !important;
  /* Suppresses pull-to-refresh entirely. User cannot force page reload via gesture.
     Combined with a consent dialog whose button is below fold, this eliminates the
     primary mobile recovery path. */
}

High mobile impact: Pull-to-refresh suppression is especially effective on mobile where browser chrome access is limited, users are accustomed to the pull gesture, and other recovery paths (right-click source view, DevTools) are unavailable. The visual effect is invisible — the gesture simply doesn't work, with no error message.

// Detection: check body-level overscroll-behavior-y for pull-to-refresh suppression
const bodyOSB = getComputedStyle(document.body).getPropertyValue('overscroll-behavior-y');
if (bodyOSB === 'none') {
  console.warn('[SkillAudit] pull-to-refresh blocked: body overscroll-behavior-y:none');
}
// Also check html element
const htmlOSB = getComputedStyle(document.documentElement).getPropertyValue('overscroll-behavior-y');
if (htmlOSB === 'none') {
  console.warn('[SkillAudit] pull-to-refresh blocked: html overscroll-behavior-y:none');
}

Attack 3: writing-mode: vertical-rl — y-axis becomes inline axis

Under writing-mode: vertical-rl, the physical y-axis becomes the inline axis (because text flows downward — in the y direction — as the primary reading direction). The block axis becomes horizontal. In this configuration, overscroll-behavior-y: none controls the inline scroll direction, while overscroll-behavior-x: none would control the block direction. An auditor who maps "y = block axis" without checking writing-mode will assign the restriction to the wrong semantic axis. The attacker sets writing-mode: vertical-rl to rotate which physical axis carries what semantic meaning, then uses the physical property to set restrictions that affect the "block" direction while bypassing logical-property audits.

/* Attack: vertical-rl writing mode — y becomes inline axis */
.consent-dialog {
  writing-mode: vertical-rl; /* y is now inline; x is now block */
  overflow-y: scroll;        /* scroll in y = scroll in the inline (reading) direction */
  overscroll-behavior-y: none; /* blocks inline-axis chain; physical y = logical inline here */
}
/* Auditor checking overscroll-behavior-block sees no restriction.
   Auditor checking overscroll-behavior-y without reading writing-mode
   thinks this is a block-axis restriction — but it controls inline under vertical-rl. */

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

The consent dialog renders normally. A mousedown listener injects overscroll-behavior-y: none on the dialog container and simultaneously resets scrollTop to 0 and increases the button's top offset, pushing it below fold. The click fires on empty dialog area. At mouseup, the injections reverse. The physical property overscroll-behavior-y may appear in auditor block-lists as a variation name rather than a known attack property, slightly reducing static analysis detection rate compared to the more commonly documented shorthand.

/* Attack: mousedown injection using physical y property */
approveBtn.addEventListener('mousedown', () => {
  dialog.style.setProperty('overscroll-behavior-y', 'none');
  dialog.scrollTop = 0;
  approveBtn.style.setProperty('top', '2000px');
});
approveBtn.addEventListener('mouseup', () => {
  dialog.style.removeProperty('overscroll-behavior-y');
  approveBtn.style.removeProperty('top');
});
// Detection: scan for both logical and physical y-axis overscroll restrictions
function auditOverscroll(el) {
  const cs = getComputedStyle(el);
  const block = cs.getPropertyValue('overscroll-behavior-block');
  const y = cs.getPropertyValue('overscroll-behavior-y');
  if (block === 'none' || block === 'contain')
    console.warn('[SkillAudit] overscroll-behavior-block:', block, el);
  if (y === 'none' || y === 'contain')
    console.warn('[SkillAudit] overscroll-behavior-y:', y, el);
}

Findings summary

High overscroll-behavior-y: none on vertically-scrollable consent dialog with approve button beyond scrollHeight — physical y-axis scroll jail; functionally equivalent to overscroll-behavior-block in horizontal-tb but evades logical-property-only audits.
High overscroll-behavior-y: none on body — pull-to-refresh suppressed; mobile users cannot force-reload page to recover from hidden consent dialog state; gesture silently fails with no feedback.
Medium writing-mode:vertical-rl: overscroll-behavior-y controls inline axis (not block); auditors mapping y=block without checking writing-mode assign the restriction to the wrong semantic direction.
High JS mousedown injects overscroll-behavior-y:none, resets scrollTop, repositions button below fold — physical property name reduces pattern-match detection rate; BCR check after mousedown is the definitive indicator.

SkillAudit checks both logical and physical overscroll properties, scans for body-level pull-to-refresh suppression, and runs a BCR sentinel during mousedown. Run a free audit on your MCP server.