Security Guide
MCP server CSS overscroll-behavior security — contain disables mobile pull-to-refresh, scroll chaining detection, parent scroll depth oracle, phishing overlay persistence
CSS overscroll-behavior (Chrome 63+, Firefox 59+, Safari 16+) controls what the browser does when a user scrolls past the end of a scroll container. The default (auto) chains the scroll to the parent container and triggers the browser's native pull-to-refresh gesture on mobile. Setting overscroll-behavior: none or contain on a scroll container stops both chain scrolling and pull-to-refresh. An MCP server that can inject CSS into a page can set overscroll-behavior: none on body to disable pull-to-refresh globally, detect the host's scroll chaining configuration, infer the depth of nested scroll containers, and prevent users from pull-to-refreshing to escape a phishing overlay injected into the page.
What overscroll-behavior controls
overscroll-behavior: auto (default) means: when the user scrolls past the end of a container, chain the scroll to the parent container, and if the outermost container is the viewport, trigger pull-to-refresh on mobile. overscroll-behavior: contain stops the scroll chain (the parent does not scroll) but still allows the overscroll visual effect (the "rubber band" stretch on iOS-style browsers). overscroll-behavior: none stops both the chain and the visual effect. Both contain and none suppress pull-to-refresh when applied to the body.
Attack 1: overscroll-behavior: contain on body disables mobile pull-to-refresh (scroll DoS)
On Android Chrome and most mobile browsers, the pull-to-refresh gesture (drag the page down when already at the top) triggers a page reload. This is the user's primary mechanism for clearing stale content, escaping injected overlays, and recovering from a page that has become unresponsive. Injecting overscroll-behavior: none on body globally disables this gesture for the entire page lifetime — the user cannot pull-to-refresh regardless of which element they attempt it on.
// MCP server CSS injection — disables pull-to-refresh globally on mobile
const style = document.createElement('style');
style.textContent = `
body {
overscroll-behavior: none; /* or: contain */
/* suppress both pull-to-refresh AND bounce/rubber-band effects */
}
`;
document.head.appendChild(style);
// The user can no longer:
// 1. Pull-to-refresh the page to reload it
// 2. Trigger the "refresh" visual feedback in Chrome for Android
// 3. Chain scrolls to the browser chrome (some browsers expose a nav gesture via overscroll)
// This is persistent for the entire page session without requiring any user interaction.
// Removing the injected element restores pull-to-refresh, but the user has no way
// to force this removal from within the page without developer tools.
Phishing persistence: Pull-to-refresh is the most reliable way a non-technical mobile user can "restart" a stuck page. Disabling it prevents the user from escaping injected content without closing the browser tab entirely — a gesture most users do not discover under pressure.
Attack 2: Scroll chaining detection via overscroll-behavior injection
The host application may have already set overscroll-behavior on some or all of its scroll containers. An MCP server can probe this configuration by injecting a test scroll container and observing whether a scroll event on the parent fires when the inner container is over-scrolled.
// Create a test inner scroll container
const probe = document.createElement('div');
probe.style.cssText = `
position: absolute; top: 0; left: 0;
width: 1px; height: 200px; /* taller than content */
overflow-y: scroll; visibility: hidden;
/* Do NOT set overscroll-behavior — inherit from parent */
`;
probe.innerHTML = 'content'; // shorter than container
document.body.appendChild(probe);
let parentScrollFired = false;
const parent = probe.parentElement;
const listener = () => { parentScrollFired = true; };
parent.addEventListener('scroll', listener, { passive: true });
// Programmatically scroll the inner container past its end
probe.scrollTop = 9999;
// If parent scroll event fires: parent has overscroll-behavior: auto (default)
// → scroll chaining is enabled → host has not hardened scroll behavior
// If parent scroll event does NOT fire: parent has overscroll-behavior: contain/none
// → host has explicitly disabled scroll chaining
setTimeout(() => {
parent.removeEventListener('scroll', listener);
probe.remove();
// parentScrollFired encodes the host's overscroll-behavior configuration
}, 100);
Attack 3: Parent scroll container depth oracle via chaining observation
In nested scroll layouts (common in mobile apps built as SPAs), multiple levels of scroll containers are stacked — an inner panel, an outer list, and the page viewport. With default overscroll-behavior: auto, scrolling past an inner container's end chains to the next outer container. An MCP server can observe how many scroll events fire on ancestors when an inner container is over-scrolled to count the depth of the scroll container hierarchy.
// Count nested scroll containers by observing scroll event chain
let scrollEventCount = 0;
const listeners = [];
// Attach scroll listeners to all potential ancestor containers
let el = document.querySelector('.main-content')?.parentElement;
while (el) {
const listener = () => { scrollEventCount++; };
el.addEventListener('scroll', listener, { passive: true });
listeners.push([el, listener]);
el = el.parentElement;
}
// Over-scroll the innermost known container
const inner = document.querySelector('.main-content');
if (inner) inner.scrollTop = 999999;
setTimeout(() => {
// scrollEventCount = number of ancestors with overscroll-behavior:auto that received
// the chained scroll — encodes scroll container nesting depth without DOM traversal
// A high count (≥3) suggests a complex nested SPA layout with multiple scroll layers.
listeners.forEach(([el, fn]) => el.removeEventListener('scroll', fn));
}, 100);
Layout fingerprint: The number of nested scroll containers is a stable fingerprint for web application framework. React Native Web, Flutter Web, and Angular Material all produce characteristic nesting depths — the scroll chain depth count can identify the host framework with high confidence.
Attack 4: Pull-to-refresh suppression for phishing UI overlays
A sophisticated MCP server attack combines CSS injection with an injected visual overlay: first, overscroll-behavior: none on body disables pull-to-refresh; then a full-viewport overlay element is injected that imitates the host application's UI but captures user credentials. Without pull-to-refresh, the user's instinctive mobile response to a suspicious page — pull down to reload — is silently disabled. Combined with disabling the browser's address bar visibility (not possible via CSS, but achievable in full-screen PWA mode), this creates a persistent phishing surface.
// Full phishing persistence setup: disable escape mechanisms + inject overlay
const setupPhishingOverlay = () => {
// Step 1: Disable pull-to-refresh
document.body.style.overscrollBehavior = 'none';
// Step 2: Disable touch-based scroll on the host page body
document.body.addEventListener('touchmove', (e) => e.preventDefault(), { passive: false });
// Step 3: Inject full-viewport overlay
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed; inset: 0; z-index: 2147483647;
background: white; overflow-y: auto;
/* Allow scrolling within the overlay — user doesn't feel trapped */
overscroll-behavior: contain; /* but chain stops here — body never sees overscroll */
`;
overlay.innerHTML = '<!-- phishing UI -->';
document.body.appendChild(overlay);
// User can scroll within the overlay but cannot pull-to-refresh the underlying page.
};
// This attack is most effective in PWA / installed web app contexts where
// the browser chrome (back button, address bar) is hidden by the manifest.
| Attack | Mechanism | Target platform | Severity |
|---|---|---|---|
| Pull-to-refresh DoS | overscroll-behavior: none on body | Mobile Chrome, mobile Firefox, mobile Safari 16+ | High |
| Scroll chaining detection | Parent scroll event observation on over-scroll | All platforms | Low |
| Scroll depth oracle | Ancestor scroll event count on chain | All platforms | Low |
| Phishing persistence | pull-to-refresh disabled + full-viewport overlay | Mobile PWA / full-screen web app | Critical |
SkillAudit findings for CSS overscroll-behavior
overscroll-behavior: none on body permanently disables the mobile pull-to-refresh gesture for the page's lifetime — removing the user's primary escape mechanism from injected page modifications.overscroll-behavior: contain/none — indicating security-conscious scroll configuration.overscroll-behavior: none with a full-viewport overlay and touchmove prevention creates a phishing surface from which a non-technical mobile user cannot escape via the usual pull-to-refresh gesture.Defences
CSP style-src 'self' blocks CSS injection: All four attacks require the MCP server to inject CSS. A strict Content Security Policy blocking inline styles without a nonce eliminates all overscroll-behavior injection attacks.
Set overscroll-behavior: contain defensively on your body: If your application explicitly sets overscroll-behavior: contain on body before any MCP server scripts run, subsequent injections of none will not take effect if the host's rule has equal or higher specificity. However, this only works if the host's rule wins the cascade — inline styles added by MCP server JavaScript (document.body.style.overscrollBehavior = 'none') override stylesheet rules regardless.
Block touchmove preventDefaulting via Trusted Types: The phishing persistence attack requires calling e.preventDefault() on touchmove events to disable the host's scroll. Trusted Types event handlers that intercept addEventListener calls for touchmove with passive: false can detect and block this pattern before the listener is registered.
Audit MCP server code for overscrollBehavior and overscroll-behavior: SkillAudit's static scanner flags any MCP server code that writes overscroll-behavior in injected CSS or modifies element.style.overscrollBehavior — both patterns are indicators of scroll DoS or phishing persistence setup.
Related: CSS scroll snap security · CSS scroll-driven animations security · CSS anchor positioning scroll security