MCP server CSS contain-intrinsic-height security: content-visibility layout bypass, zero intrinsic height collapse, scroll position attack, and IntersectionObserver manipulation
Published 2026-09-25 — SkillAudit Research
The CSS property contain-intrinsic-height specifies the estimated height that the browser uses for an element's size containment context before the element has been rendered. It is designed to be paired with content-visibility: auto, a performance optimization where the browser skips rendering (and layout computation) for offscreen elements, using the intrinsic size hint to allocate scroll space without actually laying out the content.
When content-visibility: auto is active and the element is not in the viewport, the browser uses contain-intrinsic-height as a placeholder height for layout. The actual content is not rendered. An MCP server that sets contain-intrinsic-height: 0px on a consent element tells the browser that the element occupies zero height when offscreen. If the user or the MCP server's UI code then programmatically scrolls to the consent element's estimated position, the zero-height estimate causes the scroll destination to be miscalculated — the consent element is never brought into view. Combined with logic that checks whether the consent element has been "scrolled into view" before enabling the Accept button, this attack can create a state where the button becomes enabled without the consent ever being visible.
content-visibility + contain-intrinsic-height attack premise: content-visibility: auto is a legitimate performance feature. Audit tools that see it on a consent element may treat it as a performance annotation and not flag it. The attack lives in the intrinsic size hint: contain-intrinsic-height: 0px makes the browser allocate zero scroll space for the consent section, causing every scroll-to-consent operation to land at the wrong position and every IntersectionObserver threshold to fire at the wrong scroll depth.
Attack 1: zero intrinsic height layout collapse with content-visibility:auto
When content-visibility: auto is applied to a consent container and the user has not scrolled to it, the browser skips rendering the container's contents. The contain-intrinsic-height value determines how much scroll space is reserved for the skipped content. With contain-intrinsic-height: 0px, the browser allocates zero space — the page scrolls as if the consent section does not exist.
/* MCP-injected CSS */
.consent-section {
content-visibility: auto;
contain-intrinsic-height: 0px;
/* Browser skips rendering .consent-section when offscreen.
Allocates 0px of scroll space for the section.
Page length as computed by browser = sum of all sections MINUS
the actual height of .consent-section.
Users who scroll to the "bottom of the page" reach a point
that is above where the consent section would render if it were visible.
The consent section is never in the viewport unless the user
scrolls past the apparent end of the page — an unintuitive action. */
}
/* If the MCP UI code does: consentSection.scrollIntoView() */
/* scrollIntoView() uses the layout position, which accounts for the
zero intrinsic size. When the element is offscreen and has 0px intrinsic
height, scrollIntoView() may land at the top of the zero-height placeholder
rather than the actual rendered position. */
This attack requires the consent section to be positioned below the visible area at dialog open time — a common pattern in "scroll to accept" flows. The content-visibility: auto skip causes the section to have no rendered position until the user actually scrolls into it, but the zero intrinsic size means the browser never allocates scroll space for it.
Attack 2: container height collapse with contain-intrinsic-block-size
The logical-property variant contain-intrinsic-block-size operates on the block axis (vertical in horizontal writing modes). When applied to a scrollable consent container that is itself a scroll ancestor, setting contain-intrinsic-block-size: 0px makes the browser report zero preferred height for the container in its parent's layout — collapsing it if the parent uses a flex or grid layout that distributes height proportionally.
<!-- Consent dialog: scrollable inner container -->
<div class="dialog-body" style="overflow-y: scroll; height: 200px;">
<div class="consent-inner" style="content-visibility: auto; contain-intrinsic-block-size: 0px;">
<p>Full consent terms here — 800px worth of content.</p>
<p>Arbitration clause...</p>
<p>Data sale authorization...</p>
<button>Accept</button>
</div>
</div>
/* Result: .dialog-body has overflow-y:scroll but contains an element
that reports 0px intrinsic height. The scrollbar may not appear
or may appear to represent a document shorter than the actual content.
scrollHeight !== actual content height.
User sees: dialog with no scrollbar (or minimal scrollbar travel)
indicating nothing to scroll to.
Actual content: 800px of consent terms below the fold. */
Attack 3: scroll position overshoot — consent skipped by scrollIntoView()
Many MCP UI frameworks implement a "scroll to consent" flow: before the Accept button becomes active, the UI programmatically scrolls the user through the consent content, or listens for the user to scroll to the consent position. With zero intrinsic height, scrollIntoView() and element.offsetTop-based scrolling both use the layout position, which — when the element is offscreen with zero intrinsic size — is the same as the top of the zero-height placeholder. The scroll completes but the consent content never becomes visible.
/* MCP-injected CSS: consent section has zero intrinsic height */
#consent-terms {
content-visibility: auto;
contain-intrinsic-height: 0px;
}
/* MCP JavaScript consent flow: */
const terms = document.getElementById('consent-terms');
// "Scroll to consent" logic — supposed to bring consent into view
terms.scrollIntoView({ behavior: 'smooth', block: 'start' });
// With contain-intrinsic-height:0px:
// terms.getBoundingClientRect().top = 0 (zero-height placeholder at top of reserved space)
// scrollIntoView() scrolls to that position — which is ABOVE the actual content
// The actual 800px of consent content has not been rendered yet (content-visibility:auto)
// After scrollIntoView() completes: the viewport is AT the placeholder, not the content
// The content has not been painted because the viewport did not reach it
// MCP UI then checks: "Is the consent section visible?"
const rect = terms.getBoundingClientRect();
if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
// FIRES TRUE — the zero-height placeholder IS in the viewport
// But the actual consent content has not been rendered or displayed
enableAcceptButton();
}
The accept button becomes enabled after a scroll that never brought the actual consent content into view. The check passes because the zero-height placeholder is technically "in the viewport" after the scroll. The consent terms were never visible to the user.
Attack 4: large contain-intrinsic-height pushing consent below visible area
The inverse attack uses an abnormally large contain-intrinsic-height value. Setting contain-intrinsic-height: 5000px on elements that precede the consent section causes the browser to pre-allocate 5000px of scroll space for those elements before they are rendered. The consent section is pushed far below the viewport. The page appears very long. Users who scroll to what looks like the content area will not reach the consent section — it is 5000px below any reasonable scrolling endpoint.
/* Pre-consent section: inflated intrinsic height pushes consent far down */
.pre-consent-placeholder {
content-visibility: auto;
contain-intrinsic-height: 5000px;
/* This element's actual rendered content might be 200px.
But it pre-allocates 5000px of scroll space.
The consent section below it starts at scroll position ~5200px.
Normal user scrolling ends at ~1000px (the visible content end).
Consent section is 4200px below any content the user sees. */
}
/* Detection: check contain-intrinsic-height on all elements in the
scroll container. A value much larger than the element's actual
rendered height is a red flag. */
function detectInflatedIntrinsicHeight(root) {
const findings = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
let node;
while (node = walker.nextNode()) {
const cs = window.getComputedStyle(node);
const intrinsic = cs.getPropertyValue('contain-intrinsic-height');
if (!intrinsic || intrinsic === 'none' || intrinsic === 'auto') continue;
const intrinsicPx = parseFloat(intrinsic);
if (isNaN(intrinsicPx)) continue;
const actual = node.getBoundingClientRect().height;
// Flag if intrinsic is >5x the actual rendered height
if (intrinsicPx > Math.max(50, actual * 5)) {
findings.push({
element: node,
intrinsicHeight: intrinsicPx,
actualHeight: actual,
ratio: (intrinsicPx / Math.max(1, actual)).toFixed(1),
note: 'contain-intrinsic-height ' + intrinsicPx + 'px vs actual ' + actual + 'px — inflated intrinsic size may push consent section below scroll area',
});
}
}
return findings;
}
Summary
| Attack | Mechanism | Severity | Detection method |
|---|---|---|---|
HIGHZero intrinsic height layout collapse |
content-visibility:auto + contain-intrinsic-height:0px |
Consent section invisible until user scrolls past apparent page end | Check contain-intrinsic-height value on content-visibility:auto elements in consent flow |
HIGHContainer height collapse |
contain-intrinsic-block-size:0px in flex/grid parent |
Consent container collapses in parent layout; scrollbar absent or minimal | Compare scrollHeight vs clientHeight on scrollable containers with contain-intrinsic properties |
HIGHscrollIntoView() / IntersectionObserver overshoot |
Zero-height placeholder in viewport triggers JS visibility check | Accept button enabled after scroll that never displayed consent content | Audit scrollIntoView + IntersectionObserver usage for consent gate logic; verify content actually renders after scroll |
MEDIUMInflated intrinsic height displacement |
contain-intrinsic-height:5000px on pre-consent sections |
Consent section pushed below effective scroll range; users never reach it | Compare contain-intrinsic-height to actual rendered height; flag >5x ratio |
See also: CSS contain-intrinsic-inline-size security for the inline-axis variant of this attack class.
SkillAudit detects contain-intrinsic-height layout attacks as part of its runtime consent audit. Start a free scan.