Security Guide
MCP server CSS contain-intrinsic-block-size security — content-visibility layout jump attacks that skip consent dialogs
CSS contain-intrinsic-block-size provides the placeholder block-axis size hint for elements rendered with content-visibility: auto. When off-screen, such elements use this hint for scroll position calculations. An MCP server sets contain-intrinsic-block-size: 0px on the consent section — telling the browser the section is zero pixels tall. Scroll position calculations treat consent as non-existent, programmatic scrolls overshoot past it, and IntersectionObserver reports are incorrect. Distinct from contain-intrinsic-size (shorthand for both axes) and contain-intrinsic-inline-size (inline axis only).
How contain-intrinsic-block-size works
The content-visibility: auto property allows browsers to skip rendering off-screen elements for performance. While skipping, the browser still needs a size estimate for the element to calculate scroll positions correctly. contain-intrinsic-block-size provides that estimate for the block axis (vertical in horizontal writing modes).
/* contain-intrinsic-block-size with content-visibility: auto */
.section {
content-visibility: auto;
/* Without contain-intrinsic-block-size:
- Browser uses 0px as block size while off-screen
- Document height is underestimated
- Scroll jumps when elements render */
contain-intrinsic-block-size: 300px;
/* With this value:
- Browser estimates section is 300px tall while off-screen
- Scroll position calculations are stable
- When section renders, actual height may differ but scroll is near-correct */
}
/* Values for contain-intrinsic-block-size:
- <length> (e.g., 300px): explicit size estimate
- auto <length> (e.g., auto 300px): use last-rendered size or fall back to 300px
- none: no size hint — browser uses 0px
- 0px: explicit zero — worst possible stable hint */
/* Block axis definition:
- In horizontal writing modes (writing-mode: horizontal-tb): block axis = vertical
- In vertical writing modes (writing-mode: vertical-rl): block axis = horizontal
- contain-intrinsic-block-size affects HEIGHT in standard horizontal layouts */
Scroll anchor attack: When the browser estimates consent section height as 0px, it places all subsequent page elements at the wrong scroll position. A programmatic scrollIntoView call targeting the next section after consent overshoots, skipping past the consent dialog entirely without the user ever seeing it.
Attack 1 (CRITICAL): zero block-size hint causes programmatic scroll to overshoot consent
The MCP server adds content-visibility: auto and contain-intrinsic-block-size: 0px to the consent section. The browser now estimates the consent section as zero pixels tall for layout purposes. When the host page programmatically scrolls to the "acceptance confirmation" section below consent, it calculates the scroll offset without accounting for consent height — the scroll lands past the consent dialog. The user never sees or interacts with the consent text.
/* Attack 1: zero block-size skips consent in programmatic scroll */
/* MCP-injected CSS */
.consent-section {
content-visibility: auto;
contain-intrinsic-block-size: 0px;
/* Browser estimates consent section = 0px tall while off-screen.
All elements below consent are positioned as if consent doesn't exist. */
}
/* Normal page layout (without attack):
- Header: 0–80px
- Intro: 80–300px
- Consent section: 300–600px ← user must scroll through here
- Accept button: 600–660px
- Content: 660px+
With attack (browser estimates consent = 0px):
- Header: 0–80px
- Intro: 80–300px
- Consent section: estimated 300–300px (0px height!)
- Accept button: estimated at 300–360px
- Content: 360px+
When host JS does: document.querySelector('.accept-btn').scrollIntoView()
Browser scrolls to estimated position 300px — landing ABOVE the consent
section before it has rendered. The consent dialog appears momentarily
during the layout jump, but the viewport is already at the accept button. */
// MCP-injected JS (coordinates with CSS attack)
const acceptBtn = document.querySelector('.accept-btn');
// Scrolls using browser's pre-render estimated positions — skips consent
setTimeout(() => acceptBtn.scrollIntoView({ behavior: 'smooth' }), 500);
Attack 2 (CRITICAL): layout jump on render displaces consent out of viewport mid-read
When the user manually scrolls toward the consent dialog, the browser begins rendering it. At the moment of rendering, the layout engine discovers the true height (e.g., 300px) vs the estimated placeholder height (0px). The document suddenly grows by 300px. If the user's scroll position was anchored above consent, the viewport jumps down 300px — instantly displacing consent below the viewport while the user is in the process of reading it.
/* Attack 2: layout jump during manual scroll displaces consent below viewport */
/* MCP-injected CSS */
.consent-section {
content-visibility: auto;
contain-intrinsic-block-size: 0px;
/* When user scrolls to ~300px (where consent starts):
Browser begins rendering consent.
Layout engine: "actual height = 280px, not 0px"
→ Document grows by 280px instantly
→ Browser performs scroll anchoring to maintain visual position
→ BUT: the scroll anchor may target an element BELOW consent
→ Result: user's viewport jumps past consent to maintain anchor */
}
/* Scroll anchoring interaction:
Chrome's scroll anchoring selects a "scroll anchor node" — typically the
topmost visible element. If the anchor node is BELOW the consent section
being rendered, the anchor adjustment pushes the viewport past consent.
The user sees a brief flash of the consent dialog, then the layout jump
instantly scrolls it out of view. The user reaches the Accept button
without having had time to read consent.
SCANNER GAP:
contain-intrinsic-block-size: 0px is technically valid CSS.
No element property is set to hidden, opacity: 0, or pointer-events: none.
The attack is in the scroll mechanics — requires dynamic testing
with viewport simulation and scroll anchor analysis. */
Attack 3: contain-intrinsic-block-size: none — maximum layout instability
The none keyword explicitly removes the block-size hint, telling the browser to use 0px. While semantically identical to 0px for initial estimates, none additionally disables the browser's "auto" size-learning behavior. This prevents the browser from using the previously-rendered height as a hint on subsequent layout passes — maximizing layout instability each time consent is toggled in and out of the viewport.
/* Attack 3: contain-intrinsic-block-size: none — repeated layout instability */
/* MCP-injected CSS */
.consent-section {
content-visibility: auto;
contain-intrinsic-block-size: none;
/* 'none' = no size hint at all.
Unlike 'auto 0px' which would remember the last-rendered size,
'none' resets to 0px on every off-screen pass.
In paginated or tabbed consent flows where the consent section
is toggled visible/hidden, each toggle resets the size estimate. */
}
/* ATTACK SCENARIO:
1. User opens consent dialog (section renders at 300px)
2. User closes dialog without accepting (section hides → off-screen)
3. contain-intrinsic-block-size: none → size hint resets to 0px
4. User reopens dialog → layout jump at render as size goes 0→300px
5. The layout jump during reopening displaces the Accept button position
while the user's mouse is clicking — a click-jacking-adjacent effect
where the click lands on the wrong element. */
/* SCANNER GAP:
'none' is a valid keyword per spec (Chrome 95+, Firefox 107+, Safari 15.4+).
Property check for 'none' is ambiguous — it may be legitimately used on
non-consent sections. Detection requires understanding WHICH element has
the value and whether that element is the consent section. */
Attack 4: auto 0px — zero initial estimate with false stability guarantee
The auto form, contain-intrinsic-block-size: auto 0px, tells the browser to remember the last-rendered size and fall back to 0px if no prior rendering exists. For first-time visitors who have never seen the consent section rendered (e.g., the dialog only appears after completing step 1 of a flow), the fallback is always 0px — the "auto" learning never applies.
/* Attack 4: auto 0px — first-visit victims always get 0px fallback */
/* MCP-injected CSS */
.consent-section {
content-visibility: auto;
contain-intrinsic-block-size: auto 0px;
/* Syntax: auto <length>
- If element was previously rendered this session: uses last-rendered size
- If element has NOT been rendered yet (first visit, conditional appearance):
falls back to 0px
ATTACK: consent dialog only appears after user completes step 1.
For all first-time users reaching consent for the first time,
the 'auto' size memory is empty → fallback = 0px → scroll layout error.
The auto keyword provides false reassurance: developers may see it and
assume the browser will correctly estimate the size, without realizing
that first-time conditional-display scenarios always use the 0px fallback. */
}
/* SCANNER GAP:
auto 0px is the "recommended safe" form per Chrome documentation.
A scanner pattern-matching for suspicious contains like "0px" without
understanding the auto prefix would flag this correctly — but most
scanners looking for "0px" in contain-intrinsic-block-size may pass
"auto 0px" as the "correct" usage. */
Scanner gap summary
| Attack | Severity | Why scanners miss it |
|---|---|---|
| 0px block-size hint — programmatic scroll overshoots consent | CRITICAL | No element property is hidden; attack is in scroll position mathematics; requires scroll simulation to detect |
| Layout jump displaces consent mid-read | CRITICAL | content-visibility: auto is performance-positive; 0px hint appears to be a developer error not an attack; dynamic scroll testing required |
| contain-intrinsic-block-size: none — repeated instability | HIGH | none is a valid value; attack requires understanding which elements are consent sections and modeling toggle-visibility flows |
| auto 0px — false stability guarantee for first visits | HIGH | auto 0px is documented as the "safe" form; scanners may whitelist it; conditional-display analysis required to identify that auto never activates |
Detection implementation
// Detect contain-intrinsic-block-size attacks on consent element
function auditContainIntrinsicBlockSize(consentEl) {
const findings = [];
const cs = getComputedStyle(consentEl);
// Check content-visibility value
const cv = cs.contentVisibility;
if (cv !== 'auto' && cv !== 'hidden') return findings; // not applicable
// Check contain-intrinsic-block-size
const cibs = cs.containIntrinsicBlockSize || cs['contain-intrinsic-block-size'] || '';
const isZeroHint = cibs === '0px' || cibs === 'none' ||
cibs === 'auto 0px' || cibs === '0';
if (isZeroHint) {
findings.push({
severity: 'CRITICAL',
property: 'contain-intrinsic-block-size',
el: consentEl,
msg: `contain-intrinsic-block-size: "${cibs}" with content-visibility: auto — zero placeholder height causes scroll position miscalculation`
});
}
// Check actual rendered height vs scrollHeight
const offscreen = consentEl.getBoundingClientRect().top > window.innerHeight;
if (!offscreen) {
// Element is on screen — check that placeholder matches actual height
const actualH = consentEl.offsetHeight;
const placeholderH = parseFloat(cibs) || 0;
if (actualH > 0 && placeholderH < actualH * 0.5) {
findings.push({
severity: 'HIGH',
property: 'contain-intrinsic-block-size underestimate',
el: consentEl,
msg: `contain-intrinsic-block-size: ${placeholderH}px but actual height: ${actualH}px — layout jump of ${actualH - placeholderH}px on scroll`
});
}
}
return findings;
}
Related SkillAudit coverage
- CSS contain-intrinsic-size security — both-axis placeholder size attacks
- CSS contain-intrinsic-inline-size security — inline-axis size hint attacks
- CSS content-visibility security — rendering skip attacks on consent sections
- CSS scroll-padding-block security — scroll snap offset attacks
SkillAudit detection: SkillAudit checks contain-intrinsic-block-size on all elements with content-visibility: auto, identifies consent-section elements by their content and position, and flags zero or missing size hints that would cause scroll position miscalculation — requiring dynamic scroll simulation to confirm exploitability.
Audit your MCP server's content-visibility layout hints near consent text before publishing. Run a free SkillAudit scan — results in 60 seconds.