MCP Security Reference

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

HIGHSA-CSS-CIIS-001content-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 pass
HIGHSA-CSS-CIIS-002content-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 reveals
MEDIUMSA-CSS-CIIS-003contain-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 unreadable
MEDIUMSA-CSS-CIIS-004 — JS mousedown sets consentEl.style.contentVisibility = 'hidden'; consent rendering skipped immediately; element visually disappears but display/visibility/opacity checks pass; MutationObserver on consent element style attribute required

Background: 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:

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 IDProperties involvedKey detection signal
SA-CSS-CIIS-001content-visibility:auto + contain-intrinsic-inline-size:0; zero-width placeholder collapses horizontal consent; display/visibility/opacity passcontentVisibility !== 'visible' AND parseFloat(containIntrinsicInlineSize) < 10
SA-CSS-CIIS-002content-visibility:hidden; rendering always skipped; element in DOM but never painted; display/visibility/opacity misleadgetComputedStyle(el).contentVisibility === 'hidden'
SA-CSS-CIIS-003contain-intrinsic-inline-size:1px; 1px-wide reflow collapses consent to single-character column; BCR.width=1 passes width>0 checkBCR.width < 20 AND contentVisibility !== 'visible'; flag widths under 20px
SA-CSS-CIIS-004JS mousedown sets contentVisibility:hidden inline; static: 'visible'; runtime rendering skip fires at clickMutationObserver on consent element style; re-check contentVisibility on each mutation