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
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.