MCP server CSS overflow and max-height consent security
Four SA-CSS-OVH consent attack patterns using CSS overflow, max-height, and height properties to clip consent text containers to zero or near-zero visible area. The consent text is present in the DOM and accessible to screen readers, but visually clipped by the container's box model. These attacks exploit a distinction between the element's logical content (which still exists) and its rendered visual area (which is clipped to zero).
CSS overflow: hidden clips a box to its padding edge, hiding any content that extends beyond the box boundaries. When combined with a zero or near-zero height constraint (max-height: 0, height: 0, height: 1px), this creates a container that has a layout presence (it takes up document flow) but displays zero visible content area. Consent text placed inside such a container exists in the DOM, contributes to accessibility trees, and is not flagged by checks for display: none or visibility: hidden — but no user can read it.
SA-CSS-OVH-001: max-height:0 overflow:hidden — zero visible area
max-height: 0; overflow: hidden constrains the container's maximum height to zero and clips anything overflowing. The container collapses to zero height from the perspective of both layout and painting. Any text content inside is present in the DOM but renders in a zero-height clipping region, producing no visible pixels.
.consent-container {
max-height: 0;
overflow: hidden;
}
/* Behavior:
- container.getBoundingClientRect().height: 0
- container.scrollHeight: natural content height (e.g., 40px) — text IS there
- container.clientHeight: 0 (clipped)
- Pixels rendered: 0 (entire content area clipped)
- Children accessible via DOM queries: YES
- Screen reader: reads the text (ARIA tree traverses hidden content)
- display:none check: passes (container is block with display:block)
- visibility:hidden check: passes (visible)
Max-height vs height:
max-height:0 → collapse without !important override issues
height:0 → same clip effect; may be overridden by child intrinsic size
max-height wins over height when both set: max(height, max-height) logic
Detection:
Check computed max-height: if parseFloat(getComputedStyle(el).maxHeight) === 0
AND getComputedStyle(el).overflow === 'hidden'
→ consent text clipped: SA-CSS-OVH-001 CRITICAL
Also check element.getBoundingClientRect().height on the consent container:
if height === 0 AND element has text content → CRITICAL */
scrollHeight reveals clipped text: When a consent container has max-height: 0; overflow: hidden, the container's scrollHeight is non-zero (it reflects the full content height before clipping). The clientHeight is 0. A consent auditor can detect this pattern by comparing scrollHeight > clientHeight with overflow === 'hidden' on the consent container — a ratio far greater than 1.0 indicates clipped content.
max-height: 0; overflow: hidden on consent container: zero visual area; text in DOM; scrollHeight > 0 (text is there); clientHeight = 0 (clipped). Detection: computedMaxHeight === 0 && overflow === 'hidden' → CRITICAL; also scrollHeight / clientHeight >> 1.SA-CSS-OVH-002: height:0 overflow:hidden vs overflow:visible
height: 0; overflow: hidden collapses the layout height to zero and clips content. With overflow: visible, the layout height is still zero but overflow content paints outside the box. The attack requires overflow: hidden for the clipping to be effective. A related variant uses height: 0 without explicit overflow — the default overflow: visible would leak the text out, but if the containing block also has overflow: hidden, the ancestor clips it.
.consent-container {
height: 0;
overflow: hidden; /* required for clipping — without this, text overflows visibly */
}
/* Comparison:
height:0 overflow:visible: text visually overflows BELOW the zero-height box
→ text IS visible, just repositioned; NOT an attack by itself
height:0 overflow:hidden: text clipped at box boundary; zero visible content
→ text fully hidden; ATTACK
Ancestor clipping chain:
If .consent-container has height:0 overflow:visible,
but its parent has overflow:hidden and a small height,
the overflow from .consent-container is clipped by the parent.
Detection must walk the ancestor chain for overflow clipping boundaries.
height:0 height property specificity:
If child element has min-height (e.g. min-height:20px), the child expands despite
the container height:0. But overflow:hidden on the container still clips at 0.
min-height does not override the container's clipping boundary.
Detection:
parseFloat(getComputedStyle(container).height) === 0
AND (getComputedStyle(container).overflow === 'hidden'
OR any ancestor with overflow:hidden clips this element)
→ CRITICAL */
height: 0; overflow: hidden: zero layout height; content clipped; effective only with hidden overflow (visible would leak text). Detection: computedHeight === 0 && effectiveOverflow === 'hidden' including ancestor overflow clipping chain → CRITICAL.SA-CSS-OVH-003: height:1px overflow:hidden — 1px visible slice
height: 1px; overflow: hidden restricts the visible consent area to a single pixel row at the top of the element. A 1px slice of 14px-height text shows only the top ~7% of each character glyph — the tops of ascender strokes for letters like “t”, “h”, “l”, and the dot above “i” and “j”. This slice is unrecognizable as text. The element has a non-zero getBoundingClientRect height (1px) and renders 1 row of pixels — detecting this requires checking whether the visible height is sufficient for readable text.
.consent-container {
height: 1px;
overflow: hidden;
}
/* What is visible:
For 14px font-size text in a 1px-high container:
Visible fraction: 1/14 = 7.1% of character height
At 14px, 1px = approximately the top 1px of uppercase letters.
Characters show only anti-aliased pixel smear at their top edge.
No character shape is recognizable from 1px slice.
Detection threshold:
The minimum visible container height for text legibility varies by font size.
Rule of thumb: visible height must be at least 60% of font-size to be legible.
At 14px font: min visible height = 0.6 × 14px = 8.4px → flag if height < 8.4px
Implementation:
visibleHeight = element.getBoundingClientRect().height
if (overflow === 'hidden' && visibleHeight < 0.6 * parsedFontSize) → CRITICAL
Note on padding:
height:1px on a container with padding-top:5px — the 1px height covers the top
5px padding area first. If consent text is below the padding, zero text visible.
If text starts at y=0, 1px slice of text is visible through the height clip. */
height: 1px; overflow: hidden: 1px visible slice shows 7% of character height; unrecognizable as text. Detection: visibleHeight < 0.6 × computedFontSize && overflow === 'hidden' → CRITICAL.SA-CSS-OVH-004: margin-top:-999px — off-scroll-position displacement
Negative margin-top combined with a positive height positions the consent text above its natural document flow position. With margin-top: -999px; height: 999px, the element's painted area extends from 999px above its normal position to its normal position. If the normal position is at the top of the page (e.g., the consent dialog is at the top of the viewport), the margin-top shifts the text 999px above the viewport, making it unreachable without scrolling up beyond the top of the page. The click target (the Accept button) remains at its normal position in the visible viewport.
.consent-text {
margin-top: -999px;
/* Moves the element 999px upward from its normal flow position */
/* If element was at y=100: new painted position = y=100-999 = y=-899 */
/* Text renders 899px above the top of the page — invisible to user */
}
/* Attack analysis:
Consent dialog layout:
"Consent text" element: normally at y=100
"Allow" button: at y=140 (below consent text in normal flow)
After margin-top:-999px on consent text:
Consent text element: paints at y=100-999=-899 (above page top)
"Allow" button: flow is NOT affected by the negative margin offset
(margin doesn't push following elements; it moves the element only)
Wait: actually margin-top does affect flow — negative margin pulls
following elements UP. So "Allow" button may now be at y=100 (where
consent text was), or higher.
The key: consent text is off-screen/above-viewport while the accept
button appears in the expected position. User sees button, no consent text.
Detection:
element.getBoundingClientRect().top < -element.offsetHeight
→ element entirely above viewport → CRITICAL
getComputedStyle(el).marginTop → very negative value (< -viewport_height)
→ suspicious displacement above visible area → HIGH */
Element still in layout flow with negative margin: Unlike position: absolute with a large negative top, a negative margin-top on a block element moves it upward while affecting subsequent elements' positions in the normal flow. The audit engine must check the element's rendered getBoundingClientRect() position relative to the viewport, not just its CSS margin value, to determine whether the consent text is actually visible at the time the Accept button is displayed.
margin-top: -999px: consent text displaced 999px above normal position; off-viewport at page top; Accept button remains in normal viewport position. Detection: getBoundingClientRect().bottom < 0 (element above viewport top) → HIGH; also check getComputedStyle(el).marginTop < -(viewport_height).Summary of SA-CSS-OVH findings
| ID | Severity | Properties | Visible area | Detection |
|---|---|---|---|---|
| SA-CSS-OVH-001 | CRITICAL | max-height:0 + overflow:hidden | 0px — fully clipped | maxHeight===0 && overflow=hidden |
| SA-CSS-OVH-002 | CRITICAL | height:0 + overflow:hidden | 0px — fully clipped | height===0 && effectiveOverflow=hidden |
| SA-CSS-OVH-003 | CRITICAL | height:1px + overflow:hidden | 1px — 7% of character height | visibleHeight < 0.6×fontSize |
| SA-CSS-OVH-004 | HIGH | margin-top:-999px | Full element above viewport | getBoundingClientRect().bottom < 0 |