MCP server CSS scroll and containment consent security
Four SA-CSS-SCR consent attack patterns that use CSS scroll behavior and containment sizing to position consent text below the visible area of the consent container at its default scroll position. These attacks are distinct from overflow: hidden clipping attacks: the consent text is not clipped — it is scrollable, potentially accessible to a determined user — but the default presentation ensures the user sees only the action buttons without the consent proposition. Scroll attacks are particularly deceptive because they preserve the spatial integrity of the consent UI: all elements are present in the DOM, all have correct styles, and the container has a non-zero height. Only the scroll position — a layout property rather than a CSS property — determines what is visible.
SA-CSS-SCR-001: overflow:scroll with consent text above initial scroll position
A consent dialog container with overflow: scroll or overflow-y: scroll and a fixed height smaller than its content height creates a scrollable region. The initial scroll position is determined by the DOM order of elements. If consent text is positioned above the buttons in the DOM but the container's initial scroll position is set to show only the buttons (via JavaScript scrollTop assignment or via scroll anchoring behaviors), the user sees a consent dialog with Allow/Deny buttons but no consent proposition text. The text is scrollable-to but not visible by default.
<div class="consent-container" style="
height: 60px; /* short fixed height — shows only buttons */
overflow-y: scroll;
">
<p class="consent-text">
Clicking Allow grants permanent file access to all user documents.
</p>
<button class="allow-btn">Allow</button>
<button class="deny-btn">Deny</button>
</div>
<script>
// Position scroll to show only buttons (consent text is above)
const c = document.querySelector('.consent-container');
c.scrollTop = c.scrollHeight; // scroll to bottom (buttons only visible)
</script>
/* User experience:
Sees: Allow / Deny buttons
Doesn't see: consent text (would need to scroll up)
No visual affordance: scroll-bar may be hidden on modern OS (overlay scrollbars)
Detection:
container.scrollHeight > container.clientHeight (content overflows)
AND container.scrollTop > 0 (scroll position non-zero)
AND consentTextEl.getBoundingClientRect().top < container.getBoundingClientRect().top
→ consent text is above viewport of container → CRITICAL */
Overlay scrollbars hide the affordance: On macOS and iOS, scrollbars are hidden by default (overlay scrollbars: appear briefly on scroll, then fade out). On a consent dialog the user hasn't interacted with yet, there is no visual indication that the container is scrollable. The absence of a visible scrollbar means the user has no affordance suggesting they need to scroll to see consent text above.
SA-CSS-SCR-002: scroll-snap-type:y mandatory — snap bypasses consent text
scroll-snap-type: y mandatory on a consent container with scroll-snap-align: start on the Accept button causes the browser to snap the scroll position directly to the Accept button when the dialog is opened. The snap is automatic: the browser resolves snap positions on scroll and on container creation. If the Accept button is a snap target and the consent text is not, the mandatory snap pulls the scroll position to the button, bypassing the consent text entirely. The user sees the button at the top of the container with no visible consent text.
.consent-container {
height: 80px;
overflow-y: scroll;
scroll-snap-type: y mandatory; /* mandatory: browser MUST snap to a snap target */
}
.consent-text {
scroll-snap-align: none; /* not a snap target */
}
.allow-btn {
scroll-snap-align: start; /* snap target: scroll snaps HERE */
}
/* On dialog open, the browser snaps to .allow-btn.
Scroll position = allow-btn's offsetTop.
Consent text is above this position and not visible.
If the user tries to scroll up to see consent text:
With scroll-snap-type:y mandatory, releasing the scroll immediately
snaps back to the nearest snap target (the allow-btn).
The user cannot hold the scroll in the consent-text position.
Detection:
container scroll-snap-type includes 'mandatory'
+ allow-btn has scroll-snap-align: start
+ consent text does not have scroll-snap-align (or has 'none')
→ consent text cannot be stably snapped to → CRITICAL */
SA-CSS-SCR-003: contain:size with width:0 height:0 — zero containment
contain: size on an element causes the browser to compute the element's intrinsic size independently of its content — the element does not size itself to fit its children. Combined with explicit width: 0; height: 0 on the containment container, the container collapses to zero visual area regardless of content. The consent text inside the zero-size container has layout positions computed relative to the container, but the container itself is clipped to 0×0 pixels. The text is present in the DOM, has correct computed styles, and has a non-zero scrollWidth / scrollHeight — revealing its presence — but renders no visible pixels.
.consent-text-wrapper {
contain: size; /* size containment: intrinsic size ignores content */
width: 0; /* explicit zero width */
height: 0; /* explicit zero height */
overflow: hidden; /* clip content to zero-size area */
}
.consent-text { /* inside wrapper: has layout but zero visible area */ }
/* DOM evidence of presence:
document.querySelector('.consent-text-wrapper').scrollWidth → 300 (content width)
document.querySelector('.consent-text-wrapper').scrollHeight → 48 (content height)
document.querySelector('.consent-text-wrapper').clientWidth → 0 (zero — clipped)
document.querySelector('.consent-text-wrapper').clientHeight → 0 (zero — clipped)
Detection:
getComputedStyle(el).contain includes 'size'
AND el.clientWidth === 0 AND el.clientHeight === 0
AND el.scrollWidth > 0 → consent text present but zero-clipped → CRITICAL
Simpler detection:
el.getBoundingClientRect().width === 0 AND el.getBoundingClientRect().height === 0
AND el.textContent.trim().length > 0 → zero-size but has text content → CRITICAL */
SA-CSS-SCR-004: overscroll-behavior:none — prevents recovery scroll
overscroll-behavior: none on a consent container prevents scroll chaining and overscroll effects. When combined with SA-CSS-SCR-001 (overflow:scroll with consent text scrolled out of view), overscroll-behavior: none also blocks the user from scrolling the outer page while their pointer is positioned over the consent container. This is significant: on mobile, a user might try to scroll the consent dialog area expecting to scroll the page and find the hidden content. overscroll-behavior: none also prevents the rubber-band overscroll on iOS that sometimes reveals additional content above the scroll position. As a standalone finding, overscroll-behavior: none is LOW; as a compound finding with scroll-positioning attacks, it escalates the compound severity by blocking the one user recovery action.
.consent-container {
overflow-y: scroll;
overscroll-behavior: none; /* blocks scroll chaining and overscroll reveal */
/* Combined with scrollTop set to hide consent text: */
/* user cannot scroll out of container to page, */
/* and cannot overscroll to reveal text above */
}
/* Standalone severity: LOW (overscroll behavior alone is not harmful)
Compound severity with SA-CSS-SCR-001: HIGH
(blocks recovery from scroll-positioning attack)
Detection for compound finding:
overscroll-behavior: none/contain
AND container.scrollTop > 0
AND consent text above container viewport
→ compound CRITICAL (SCR-001 + SCR-004 together) */
Compound scoring for scroll attacks: No individual scroll property in this family achieves maximum severity alone. The attack surface comes from combinations: a fixed-height container with overflow-y: scroll (SA-CSS-SCR-001) becomes CRITICAL only when the scroll position is set to hide consent text. Adding overscroll-behavior: none (SA-CSS-SCR-004) blocks the user's primary recovery action. Auditors scoring only individual properties will under-report the compound severity of these attack chains.
Summary of SA-CSS-SCR findings
| ID | Severity | Properties | Attack mechanism | Detection |
|---|---|---|---|---|
| SA-CSS-SCR-001 | CRITICAL | overflow:scroll + fixed height + scrollTop set | Consent text below initial scroll position; buttons visible, text not | scrollHeight > clientHeight + scrollTop > 0 + text above viewport |
| SA-CSS-SCR-002 | CRITICAL | scroll-snap-type:y mandatory + snap-align on Allow | Mandatory snap jumps to Allow button; consent text not a snap target | mandatory snap + allow-btn snap target + consent text not a snap target |
| SA-CSS-SCR-003 | CRITICAL | contain:size + width:0 height:0 + overflow:hidden | Zero-size containment area clips consent text; scrollWidth > 0 reveals presence | contain:size + clientWidth/Height === 0 + scrollWidth > 0 |
| SA-CSS-SCR-004 | MEDIUM | overscroll-behavior:none | Blocks scroll chaining/recovery from scroll-positioning attacks | Compound: overscroll:none + scrollTop > 0 + text above viewport |