MCP server CSS contain-intrinsic-inline-size security
CSS Containment Level 2 introduced contain-intrinsic-inline-size as the inline-axis sub-property of contain-intrinsic-size. When content-visibility: auto is active, the browser skips layout and rendering for off-screen elements, using the contain-intrinsic-size value as a placeholder to reserve scroll space. Setting contain-intrinsic-inline-size: 0 tells the browser to reserve zero horizontal pixels — the element takes up no horizontal space in the document flow when off-screen. Combined with overflow: hidden on the consent container, a consent dialog that scrolls horizontally has its width collapsed to 0 before it enters the viewport, and may never expand to a readable width. The contentVisibility and containIntrinsicInlineSize properties are the detection signals — standard width checks on the element can mislead.
Attack findings
content-visibility: auto; contain-intrinsic-inline-size: 0 on consent element; browser reserves 0px horizontal placeholder space when element is off-screen; consent in horizontal-scroll layout has zero width when it enters viewport intersection threshold; BCR.width = 0 at scroll position; textContent intact; display and visibility checks passcontent-visibility: hidden; contain-intrinsic-inline-size: 0 on consent element; content-visibility:hidden skips rendering entirely (stronger than auto); element occupies 0px inline space; never rendered regardless of scroll; display:block visibility:visible opacity:1 all pass; only contentVisibility check revealscontain-intrinsic-inline-size: 1px (minimal nonzero) with content-visibility: auto; browser reserves 1px horizontal space; consent text reflows into 1px-wide column: extremely tall single-character column; BCR.width = 1; BCR.height extremely large; visually illegible; width check > 0 passes but content unreadableconsentEl.style.contentVisibility = 'hidden'; consent rendering skipped immediately; element visually disappears but display/visibility/opacity checks pass; MutationObserver on consent element style attribute requiredBackground: CSS Containment Level 2 and content-visibility
CSS Containment Level 2 (content-visibility) allows the browser to skip layout and rendering for off-screen elements as a performance optimization. The property has three values:
- visible (default) — normal rendering, no containment benefit.
- auto — the browser skips layout/rendering when the element is off-screen. When it enters the viewport, the browser renders it normally. While off-screen, the
contain-intrinsic-sizevalue is used as a placeholder to maintain scroll position. - hidden — the browser always skips rendering, similar to
visibility: hiddenbut more complete: it skips layout, paint, and compositing. The element's content is invisible and inaccessible. Unlikedisplay: none, the element still occupies space in the document (defined bycontain-intrinsic-size).
The contain-intrinsic-inline-size property (and its block-axis counterpart contain-intrinsic-block-size) are the per-axis versions of contain-intrinsic-size. Setting contain-intrinsic-inline-size: 0 tells the browser the element's inline (horizontal) size placeholder is 0px.
Detection gap: When content-visibility: auto is active and the element is off-screen, getComputedStyle(el).width returns the placeholder value, not the rendered width. When content-visibility: hidden is active, display, visibility, and opacity all return normal values — the element appears visible to those checks. The detection signals are getComputedStyle(el).contentVisibility (checking for 'auto' or 'hidden') and getComputedStyle(el).containIntrinsicInlineSize (checking for '0px' or suspiciously small values).
Attack 1 — content-visibility:auto with zero inline placeholder (SA-CSS-CIIS-001)
In a horizontal-scroll consent layout (consent text displayed in a horizontally scrollable container), content-visibility: auto; contain-intrinsic-inline-size: 0 on the consent paragraph tells the browser: "when this element is outside the visible area, reserve 0px of horizontal space for it." The consent container scrolls to show the consent, but the element's placeholder is 0px wide — when the browser's intersection observer determines the element is at the viewport edge (triggering content-visibility:auto rendering), it may allocate the actual width correctly. However, if combined with overflow: hidden on the parent and the parent's layout is based on the placeholder size, the consent text never gets a non-zero inline size during the layout phase when the button is clicked.
/* Attack */
.consent-dialog {
overflow: hidden;
width: 100vw;
}
.consent-text {
content-visibility: auto;
contain-intrinsic-inline-size: 0;
/* browser reserves 0px horizontal for consent when off-screen */
/* horizontal scroll shows "consent" at 0px width = zero content visible */
/* display: block; visibility: visible; opacity: 1 — all pass */
}
/* Detection */
function checkContainIntrinsicInline(consentEl) {
const cs = getComputedStyle(consentEl);
const cv = cs.contentVisibility;
if (!cv || cv === 'visible') return null;
const ciis = cs.containIntrinsicInlineSize;
if (!ciis) return null;
const pxVal = parseFloat(ciis);
if (pxVal === 0 || pxVal < 10) {
return {
vuln: 'SA-CSS-CIIS-001',
severity: 'high',
detail: `content-visibility: ${cv}; contain-intrinsic-inline-size: ${ciis} — zero/minimal inline placeholder collapses consent width`
};
}
return null;
}
SA-CSS-CIIS-001 (High). content-visibility: auto; contain-intrinsic-inline-size: 0 — zero inline placeholder collapses consent width in off-screen phase. Detection: getComputedStyle(el).contentVisibility !== 'visible' combined with parseFloat(getComputedStyle(el).containIntrinsicInlineSize) < 10.
Attack 2 — content-visibility:hidden skips rendering entirely (SA-CSS-CIIS-002)
content-visibility: hidden is stronger than auto: the browser always skips layout and rendering for the element, regardless of its position relative to the viewport. The element's content is never painted. Unlike display: none, the element still participates in document flow and occupies the space defined by contain-intrinsic-size. Unlike visibility: hidden, the element cannot receive focus and is not accessible to assistive technologies.
An MCP server sets content-visibility: hidden on the consent paragraph. The consent area appears as blank space in the dialog. The install button — in a separate element without content-visibility: hidden — is fully visible and clickable. The check: getComputedStyle(el).display returns 'block'; visibility returns 'visible'; opacity returns '1'. None of these catch the attack. Only getComputedStyle(el).contentVisibility === 'hidden' reveals it.
/* Attack */
.consent-text {
content-visibility: hidden;
contain-intrinsic-inline-size: 0;
/* element exists in DOM, occupies 0px inline space */
/* rendering completely skipped */
/* display: 'block', visibility: 'visible', opacity: '1' — all mislead */
}
/* Detection */
function checkContentVisibility(consentEl) {
const cv = getComputedStyle(consentEl).contentVisibility;
if (cv === 'hidden') {
return {
vuln: 'SA-CSS-CIIS-002',
severity: 'critical',
detail: `content-visibility: hidden — consent rendering permanently skipped; element not painted`
};
}
return null;
}
SA-CSS-CIIS-002 (High). content-visibility: hidden always skips element rendering. The consent text is never painted. Standard visibility checks (display, visibility, opacity) all return normal values. Detection: getComputedStyle(el).contentVisibility === 'hidden'.
Attack 3 — minimal 1px inline size reflows consent into single-character column (SA-CSS-CIIS-003)
Setting contain-intrinsic-inline-size: 1px (instead of 0) passes the width > 0 check but still collapses the inline size to 1px. A consent text with a 1px inline constraint reflows its text into a single-character-wide column that may be hundreds of pixels tall. BCR.width = 1, BCR.height = very large (all consent lines stacked in a 1px-wide column). The consent is technically visible (non-zero area, in-viewport) but practically unreadable. A BCR width check that only ensures width > 0 passes; a check for width < 20 catches it.
Attack 4 — JS mousedown content-visibility:hidden injection (SA-CSS-CIIS-004)
At page load, the consent element has content-visibility: visible (or unset). At mousedown, JS sets consentEl.style.contentVisibility = 'hidden'. The browser immediately stops painting the consent element. The click event fires with the consent hidden. MutationObserver on the consent element's style attribute catches the inline style change; re-checking contentVisibility on mutation detects the attack before the click event propagates.
/* Runtime monitor */
new MutationObserver((mutations) => {
for (const m of mutations) {
const cs = getComputedStyle(consentEl);
const cv = cs.contentVisibility;
if (cv === 'hidden' || cv === 'auto') {
const ciis = parseFloat(cs.containIntrinsicInlineSize || '0');
if (cv === 'hidden' || ciis < 10) {
flagTampering('SA-CSS-CIIS-004');
installBtn.disabled = true;
}
}
}
}).observe(consentEl, { attributes: true, attributeFilter: ['style', 'class'] });
SkillAudit detection: SkillAudit checks getComputedStyle(el).contentVisibility and getComputedStyle(el).containIntrinsicInlineSize on all consent elements. content-visibility: hidden is flagged as Critical. content-visibility: auto with containIntrinsicInlineSize < 10px is flagged as High. Runtime injection is caught via MutationObserver. Run a free audit →
Detection summary
| Attack ID | Properties involved | Key detection signal |
|---|---|---|
| SA-CSS-CIIS-001 | content-visibility:auto + contain-intrinsic-inline-size:0; zero-width placeholder collapses horizontal consent; display/visibility/opacity pass | contentVisibility !== 'visible' AND parseFloat(containIntrinsicInlineSize) < 10 |
| SA-CSS-CIIS-002 | content-visibility:hidden; rendering always skipped; element in DOM but never painted; display/visibility/opacity mislead | getComputedStyle(el).contentVisibility === 'hidden' |
| SA-CSS-CIIS-003 | contain-intrinsic-inline-size:1px; 1px-wide reflow collapses consent to single-character column; BCR.width=1 passes width>0 check | BCR.width < 20 AND contentVisibility !== 'visible'; flag widths under 20px |
| SA-CSS-CIIS-004 | JS mousedown sets contentVisibility:hidden inline; static: 'visible'; runtime rendering skip fires at click | MutationObserver on consent element style; re-check contentVisibility on each mutation |