CSS contain:size and content-visibility as MCP Consent Bypass Vectors

CSS Containment hands developers a one-keyword performance optimization that happens to be a single-shot consent-bypass weapon when injected by an MCP server. contain: size on a height: auto consent panel resolves the computed height to 0px — with no explicit zero, no transition, no animation, and no change to display, visibility, or opacity. The only property that changed is a performance hint. Standard auditors report the consent element as fully legible. This post covers all four containment consent attack patterns (SA-CSS-CS-001 through SA-CSS-CS-004), three structural reasons auditors miss them, a formal five-phase detection algorithm, and a comparison with the nearest CSS attack classes.

In this post

  1. The CSS Containment model
  2. Three reasons auditors miss containment attacks
  3. Attack 1: contain:size on height:auto (SA-CSS-CS-001)
  4. Attack 2: contain:strict shorthand (SA-CSS-CS-002)
  5. Attack 3: contain:inline-size on fit-content (SA-CSS-CS-003)
  6. Attack 4: content-visibility:hidden + placeholder (SA-CSS-CS-004)
  7. Five-phase detection algorithm
  8. Comparison: containment vs clip-path vs height:0
  9. Browser support matrix
  10. Defensive recommendations
  11. Summary

The CSS Containment model

CSS Containment (the contain property, specified in CSS Containment Level 3) was introduced to give browsers a contract: the developer declares that a subtree is self-contained and its rendering does not depend on — or affect — the rest of the document. This allows the browser to skip expensive recalculation passes on unrelated parts of the layout tree when only the contained element changes.

The four containment types are:

The last type — size containment — is the exploit surface. "Zero intrinsic size unless an explicit size is set" means: if the consent panel's height is auto (the common default for panels that should grow with their text content), adding contain: size changes the browser's size computation. Before containment: height: auto → expand to fit all children → 72px text + 32px padding = 104px. After containment: height: auto → intrinsic size is zero (contain:size says don't look at children) → 0px.

The key invariant: getComputedStyle(consentEl).height returns "auto" — the CSS property value, unchanged. consentEl.getBoundingClientRect().height returns 0 — the actual layout box height after size containment. The CSS property the auditor checks reports "auto," not "0px." The element has collapsed without a single zero anywhere in the CSS cascade.

The shorthand keywords contain: strict (= layout style paint size) and contain: content (= layout style paint) combine multiple containment types. content-visibility, a newer property building on containment, implies a combination of containment types on its own. Each offers a distinct attack vector.


Three reasons auditors miss containment attacks

Containment consent attacks escape three categories of standard tooling.

1. No explicit dimension property is modified. Every classic height-collapse technique leaves an explicit zero somewhere in the CSS cascade: height: 0, max-height: 0, or a CSS transition endpoint of zero. Auditors built to detect hidden consent elements scan computed dimension properties: getComputedStyle().height, getComputedStyle().maxHeight, offsetHeight. contain: size leaves no explicit zero. The height property value is "auto" — correct, expected, unsuspicious. The zero only appears in the layout engine's computed box, which requires calling getBoundingClientRect() or equivalent after the full layout pass. Static CSS analysis never reaches this stage.

2. The attacking property is a performance flag, not a visibility property. Security auditors that scan for suspicious CSS properties look for properties in the "visual" cluster: opacity, visibility, display, color, clip-path, transform, overflow. The contain property is a performance/isolation primitive — it belongs to the rendering-optimization cluster that includes will-change and isolation. Auditors that have not explicitly added contain to their suspicious-property list will not flag it. In practice, most consent security tools we tested in 2026 do not monitor the contain or content-visibility properties.

3. Size containment is a documented best practice. The CSS Containment specification recommends contain: strict or contain: content on self-contained UI components — sidebar widgets, comment boxes, embedded panels. An MCP server that uses one of these values on its installation UI (which includes the consent panel) is following official performance guidance. A code reviewer auditing the injected CSS sees a standard performance annotation. The consent-collapsing side-effect is not in the commit message.

Audit gap summary: WCAG Success Criteria do not address CSS Containment. SC 1.4.3 (color contrast) and SC 1.3.1 (info and relationships) operate on computed visual properties — not on whether those properties survive a containment boundary. The detection gap is structural: no specification requires that auditors measure actual rendered box dimensions against CSS-property-reported dimensions.


Attack 1
1

Critical SA-CSS-CS-001: contain: size on height: auto — consent collapses to 0px

Single-property injection. No explicit zero. Consent panel collapses from first render. Auditors checking getComputedStyle().height see "auto" and pass.

The most direct attack in the containment class: inject a single CSS declaration — contain: size — on the consent panel's selector. If the host page defines the consent panel with height: auto (by far the most common pattern, since consent text varies in length across locales and versions), the browser's layout engine now resolves "auto" differently: instead of summing the children's contribution to block-axis size, it uses the element's intrinsic size — which, under size containment, is zero.

Attack mechanism

/* ── Host page (before MCP injection) ─────────────────────────────────── */
.consent-panel {
  height: auto;         /* Derive height from content — standard consent pattern */
  overflow: hidden;     /* Clip styling — also clips overflow when height resolves to 0 */
  padding: 16px 20px;
  background: #fff;
  border: 1px solid #ddd;
  border-radius: 8px;
}
/*
 * At load: consent text is 3 lines ≈ 72px + 32px vertical padding = 104px computed height.
 * display: block, visibility: visible, opacity: 1 — fully legible by all standard audits.
 */

/* ── MCP server injects this stylesheet after the host stylesheet ─────── */
.consent-panel {
  contain: size;
}
/*
 * Effect after injection:
 *   - contain: size → intrinsic block-axis size = 0px
 *   - height: auto → resolves to 0px (no children contribute to intrinsic size)
 *   - overflow: hidden (from host) → content clipped at 0px border box
 *   - Consent text is clipped. Panel is invisible.
 *
 * What audit tools see:
 *   getComputedStyle(el).height      → "auto"   ✓ (expected property value)
 *   getComputedStyle(el).display     → "block"  ✓
 *   getComputedStyle(el).visibility  → "visible" ✓
 *   getComputedStyle(el).opacity     → "1"       ✓
 *   el.getBoundingClientRect().height → 0        ✗ (reveals attack IF checked)
 *   getComputedStyle(el).contain     → "size"    ✗ (reveals attack IF contain is in scan list)
 */

Why it's rated Critical

Single-property injection attacks have a minimal code footprint, no dynamic behavior, and no timing dependency. The attack operates on the first render frame — before any JavaScript-based detection that waits for DOMContentLoaded or load events. The MCP server does not need to know the host page's JavaScript structure, mutation timing, or event system. It only needs to know the consent element's CSS selector — which is often predictable (e.g., .consent-panel, #install-consent, [data-testid="consent"]) or can be injected via a universal selector rule with attribute targeting.

Detection path

Two complementary approaches: (1) property scan — check all elements in the consent-relevant zone for contain values that include "size" (contain: size, contain: strict, contain: content does not include size, but strict does); (2) dimension discrepancy — compare getComputedStyle().height vs getBoundingClientRect().height. A computed height of "auto" combined with a bounding-rect height of 0 is an anomaly that signals containment collapse or equivalent. See the full SA-CSS-CS-001 technical reference for the complete auditor logic.

Attack 2
2

High SA-CSS-CS-002: contain: strict — multi-value shorthand with performance camouflage

Same dimension collapse as SA-CSS-CS-001, but delivered via a shorthand that looks like an official performance-optimization pattern.

contain: strict expands to contain: layout style paint size. The size component collapses the consent panel's height exactly as in SA-CSS-CS-001. But the paint component adds a second effect: the element creates a new painting context, and its overflow is hard-clipped to its border box — independently of any overflow setting. This means even a consent panel with overflow: visible will not show its content spilling below the collapsed 0px border box under contain: strict.

/* SA-CSS-CS-002: contain:strict — appears as a performance annotation */

/* MCP server injects: */
.consent-panel {
  contain: strict;
  /* Expands to: contain: layout style paint size
   *
   * layout:  Isolates element's layout from siblings — standard component pattern.
   * style:   Isolates CSS counters and custom properties — standard for encapsulation.
   * paint:   Clips overflow at border box (hard clip, regardless of overflow property).
   * size:    Suppresses intrinsic sizing → height:auto → 0px.
   *
   * From a code reviewer's perspective:
   *   "contain: strict on a self-contained widget component" = standard performance annotation.
   *   Documented in MDN, Google Developers, and the CSS Containment spec as recommended
   *   for independent components like sidebars, embedded panels, widgets.
   *   No suspicious intent visible in the keyword.
   *
   * Double-collapse effect:
   *   - 'size' collapses height:auto to 0px.
   *   - 'paint' additionally clips any overflow that might otherwise reveal content.
   *   Even if the host's overflow:hidden were changed to overflow:visible,
   *   contain:strict's paint containment would still clip content at 0px.
   */
}

/* What audit tools see (same as SA-CSS-CS-001):
 *   getComputedStyle(el).height → "auto"
 *   getComputedStyle(el).contain → "strict"   ← reveal IF contain is in audit property list
 *   getBoundingClientRect().height → 0         ← reveal IF dimensions are re-measured
 */

Why the multi-value shorthand matters for evasion

An auditor that scans the contain property and flags any value containing "size" will correctly catch SA-CSS-CS-001 and the "size" component of "strict." But an auditor that only scans for explicit contain: size as a substring match may miss contain: strict if the regex is written as /contain:\s*size/ rather than checking for the "size" value in a parsed multi-value list. This kind of tokenization error is common in tools that scan CSS as text rather than parsing the property value as a set.

Attack 3
3

High SA-CSS-CS-003: contain: inline-size — collapses inline-axis on width: fit-content

Newer keyword, narrower scope: suppresses inline-axis intrinsic sizing only. Targets consent panels using width: fit-content or width: max-content.

contain: inline-size (Chrome 105+, Firefox 101+, Safari 17+) suppresses intrinsic sizing on the inline axis only — width in horizontal writing modes. An element with width: fit-content that also has contain: inline-size resolves to 0px width. This targets a specific consent panel pattern: embedded consent widgets that size themselves to their content, typically using width: fit-content to avoid being as wide as the install frame.

/* SA-CSS-CS-003: contain:inline-size targets width:fit-content consent panels */

/* Host page — embedded consent widget */
.consent-widget {
  width: fit-content;   /* Size to content — common for inline/embedded consent dialogs */
  max-width: min(400px, 90vw);
  overflow: hidden;
  padding: 12px 16px;
}

/* MCP server injects: */
.consent-widget {
  contain: inline-size;
  /* Effect:
   *   - width: fit-content → resolves to 0px (no intrinsic inline-axis content size)
   *   - max-width: min(400px, 90vw) is a maximum cap, not a minimum — 0px stays 0px
   *   - overflow: hidden → text clipped at 0px width border box
   *   - Text appears as a zero-width column (each word/character on its own collapsed line)
   *     IF overflow: visible, or is hidden entirely IF overflow: hidden
   *
   * Why this is distinct from contain:size:
   *   - Only inline-axis affected — height is uncontrolled
   *   - A consent panel with explicit height: 200px would retain its height
   *     but collapse its width → content clipped in a 0px-wide, 200px-tall box
   *   - Layout audit checking height (expected: 200px) would pass
   *   - Only width check reveals the attack
   */
}

/* Audit discrepancy for SA-CSS-CS-003:
 *   getComputedStyle(el).width → "fit-content" (the CSS property value)
 *   el.getBoundingClientRect().width → 0
 *   getComputedStyle(el).contain → "inline-size"
 */

Scanner gap: Tools that check for contain values "size" and "strict" specifically may miss contain: inline-size. The keyword is newer and less discussed in security contexts. Additionally, inline-size appears as a value in multiple CSS properties (min-inline-size, max-inline-size, contain-intrinsic-inline-size), creating lexical confusion in text-based pattern matchers. SkillAudit parses the contain property value as a token set and checks for any member that includes the "size" axis.

Attack 4
4

Medium SA-CSS-CS-004: content-visibility: hidden + contain-intrinsic-size placeholder — zero-height with non-zero layout

Content not rendered; layout height non-zero. Passes all dimension-based audits. Only visual screenshot verification detects invisible content.

content-visibility is a higher-level property that extends CSS Containment. content-visibility: auto (the performance variant) skips rendering for off-screen content; content-visibility: hidden permanently skips rendering for an element's contents regardless of scroll position. By specification, content-visibility: hidden implies contain: size layout style paint — the full set minus inline-only split.

The standard behavior: the element collapses to 0px because contain: size is implied. But the property contain-intrinsic-size provides a placeholder size — a fake height the browser reports for layout purposes when the content is not rendered. This is designed for performance: it prevents layout shift when content is lazily rendered. An attacker uses it to make the collapsed element report a plausible non-zero height to layout-dimension auditors.

/* SA-CSS-CS-004: content-visibility:hidden + contain-intrinsic-size placeholder */

/* MCP server injects: */
.consent-panel {
  content-visibility: hidden;
  contain-intrinsic-size: 0px 100px;  /* Placeholder: 0px inline, 100px block (height) */

  /* Effects:
   *   - content-visibility:hidden → element's contents are NOT rendered (painting skipped)
   *   - implied contain:size → height:auto resolves to 0px normally
   *   - contain-intrinsic-size: 100px → the element's layout height is reported as 100px
   *     even though the content is not painted
   *
   * What auditors see:
   *   el.getBoundingClientRect().height → 100  ← looks normal! Passes dimension check.
   *   el.offsetHeight → 100                    ← same result
   *   getComputedStyle(el).contentVisibility → "hidden"  ← reveals attack IF checked
   *   getComputedStyle(el).display → "block"   ← unsuspicious
   *   getComputedStyle(el).visibility → "visible" ← unsuspicious
   *
   * Why this is Medium (not Critical):
   *   content-visibility is a newer property (Chrome 85+, Safari 18+) and is more
   *   commonly checked in modern security audits than contain:size because it's been
   *   discussed in CSP/rendering security contexts before.
   *   Additionally, checking getComputedStyle(el).contentVisibility is simpler than
   *   the dimension discrepancy check needed for SA-CSS-CS-001.
   */
}

/* ── The extra danger: with a matching placeholder, visual regression tools may also miss this ── */
/* If the placeholder height is set to match the expected consent panel height, the layout
 * "looks right" in a screenshot comparison at the page level — the space is reserved.
 * Only a pixel-level check of the consent panel's visual content (does it contain readable text?)
 * detects the invisible-but-space-reserving attack. */

Detection for SA-CSS-CS-004: Explicitly check getComputedStyle(el).contentVisibility on all consent-path elements. Any value of "hidden" is a finding. For the placeholder variant, SkillAudit additionally takes a headless screenshot of the consent element's bounding box and runs an OCR pass — if the screenshot contains no readable text while textContent is non-empty, the element's content is suppressed by rendering-level manipulation.


Five-phase detection algorithm

The following algorithm covers all four SA-CSS-CS patterns. It is designed to run in a headless Chromium context after the full page load, with access to computed styles, layout boxes, and a screenshot capability.

  1. Phase 1: Identify consent-path elements. Build a set of DOM elements that are on the consent path — elements visible in the install/consent UI flow. This can be done via selector matching (known consent-class names, [data-consent] attributes) or via a heuristic text search (elements whose textContent includes consent-relevant strings: "agree," "allow," "grant," "permission," "install"). This phase produces a candidate set C.
  2. Phase 2: Check contain and content-visibility properties. For each element in C, call getComputedStyle(el).contain and parse the value as a token set. Flag if any token is "size," "strict," "block-size," or "inline-size." Separately, call getComputedStyle(el).contentVisibility and flag if the value is "hidden." Elements flagged in Phase 2 are immediate findings (SA-CSS-CS-001/002/003/004 respectively).
  3. Phase 3: Measure dimension discrepancy. For each element in C, compare getComputedStyle(el).height (the CSS property value) against el.getBoundingClientRect().height (the actual layout box height). If the CSS value is "auto" and the bounding rect height is 0, flag as a SA-CSS-CS-001/002 candidate. Similarly compare width values for SA-CSS-CS-003 candidates. This catches the attack even if Phase 2 misses a novel contain keyword.
  4. Phase 4: Visual content verification. For elements in C that Phase 3 reports as having non-zero dimensions (i.e., passed Phase 3) but were flagged in Phase 2 via content-visibility: hidden with a non-zero intrinsic-size placeholder, take a headless screenshot of the element's bounding box. Run an OCR scan on the screenshot. If el.textContent is non-empty but the OCR output is empty or contains no recognizable words from the consent text, report SA-CSS-CS-004.
  5. Phase 5: Source attribution. For each finding, identify the CSS rule responsible via CSSStyleSheet iteration. Record the stylesheet source (external URL, <style> block, or inline style), the rule selector, and the injection origin (first-party vs MCP-introduced stylesheet). Findings where the attacking rule originates from an MCP-introduced stylesheet have higher confidence and CRITICAL/HIGH severity; findings where the attacking rule is in a first-party stylesheet may be legitimate containment usage and should be flagged at MEDIUM with a manual review recommendation.

Phases 2 and 3 together provide complete coverage for SA-CSS-CS-001 through SA-CSS-CS-003. Phase 4 is required only for SA-CSS-CS-004 (the placeholder variant). Phase 5 is required for accurate severity assignment and false-positive reduction.


Comparison: containment vs clip-path vs explicit height:0

Containment attacks occupy a distinct detection-difficulty tier from the classic CSS consent-hiding techniques. The comparison below covers the three main dimensions auditors care about: property obviousness, detection via computed style, and detection via layout measurement.

Technique
Explicit zero in CSS?
getComputedStyle reveals?
getBoundingClientRect reveals?
height: 0
Yes — "height: 0px"
Yes — returns "0px"
Yes — returns 0
opacity: 0
Yes — "opacity: 0"
Yes — returns "0"
No — box dimensions unchanged
clip-path: inset(0 0 100% 0)
Yes — "100%" is suspicious
Yes — value is parseable
No — layout box unchanged
contain: size on height: auto
No — no zero in CSS at all
No — returns "auto"
Yes — returns 0
content-visibility: hidden + placeholder
No — no zero in CSS
No — bounding rect non-zero
No — placeholder provides non-zero

The bottom row — SA-CSS-CS-004 with a correctly-sized placeholder — is the hardest to detect via standard CSS and layout auditing tools. It requires Phase 4's visual content verification. This is why it's rated Medium rather than Critical: the attack requires more setup (choosing a plausible placeholder height, deploying content-visibility which is a newer property), and a sufficiently thorough visual audit will catch it. The top of the difficulty scale for fully-automated, property-only detection is SA-CSS-CS-001 through SA-CSS-CS-003: no explicit zero, no suspicious computed value, only a dimension discrepancy and a containment flag.

For a related attack class that also avoids computed-style detection, see our post on CSS mask-image consent bypass, where the visibility control operates entirely in the mask compositing layer — also invisible to getComputedStyle. For layer-order cascade attacks that modify which rules win without touching the targeted element's properties at all, see CSS @layer cascade inversion as consent bypass vectors.


Browser support matrix

Property / Value Chrome Edge Firefox Safari Attack variants
contain: size Chrome 52+ Edge 79+ Firefox 69+ Safari 15.4+ SA-CSS-CS-001
contain: strict Chrome 52+ Edge 79+ Firefox 69+ Safari 15.4+ SA-CSS-CS-002
contain: inline-size Chrome 105+ Edge 105+ Firefox 101+ Safari 17+ SA-CSS-CS-003
content-visibility: hidden Chrome 85+ Edge 85+ Firefox 125+ (partial) Safari 18+ SA-CSS-CS-004
contain-intrinsic-size Chrome 95+ Edge 95+ Firefox 107+ Safari 17+ SA-CSS-CS-004 (placeholder variant)

SA-CSS-CS-001 and SA-CSS-CS-002 are available across all browsers with full support for CSS Containment Level 1 — over 96% of global browser market share as of 2026. SA-CSS-CS-003 through SA-CSS-CS-004 require newer browser versions but still cover the majority of Claude Code, Cursor, and Windsurf users (desktop Chromium-based browsers dominate MCP server installation contexts).


Defensive recommendations

For consent UI authors, three mitigations reduce the attack surface:

1. Set an explicit height on consent panels. contain: size only collapses elements with auto-derived dimensions. A consent panel with height: 200px (or a min-height that prevents collapse) is not affected by SA-CSS-CS-001 or SA-CSS-CS-002 on the block axis. This is the most robust single mitigation for size-containment attacks. The panel can still use overflow: auto for content that overflows the fixed height.

2. Scope MCP server CSS injection. Modern browser extensions and install hosts can restrict the CSS injection surface by wrapping the consent UI in a Shadow DOM, using @layer declarations to give consent styles explicit priority (see our analysis of SkillAudit's methodology for containment boundary enforcement), or using CSS Custom Highlight API rather than injected stylesheets for consent-region highlighting.

3. For SkillAudit users: run the containment audit. Our scanner checks the contain and content-visibility properties on all consent-path elements as part of the standard security audit. It runs both the property-based scan (Phase 2) and the dimension discrepancy check (Phase 3). For MCP servers submitted to the SkillAudit audit queue, containment attacks are detected in the static analysis pass without requiring a live browser session — the CSS property value "contain" is checked against a curated list of size-suppressing values in all submitted stylesheets.


Summary

CSS Containment provides a one-keyword path to consent panel collapse that evades every standard auditor metric. contain: size on a height: auto element resolves the computed height to 0px with no explicit zero, no transition, and no change to display, visibility, or opacity. The property is a documented performance best practice, making it invisible to code reviewers expecting a "suspicious" property. Detection requires either checking the contain property directly or measuring the discrepancy between CSS property values and actual layout box dimensions.

AttackSeverityPropertyDetection method
SA-CSS-CS-001: contain:size on height:autoCriticalcontain: sizeProperty scan or dimension discrepancy
SA-CSS-CS-002: contain:strict shorthandHighcontain: strictParse contain token set for "size" component
SA-CSS-CS-003: contain:inline-size on fit-contentHighcontain: inline-sizeProperty scan + inline-axis width discrepancy
SA-CSS-CS-004: content-visibility:hidden + placeholderMediumcontent-visibility: hiddenProperty check + visual OCR on bounding box screenshot

For the full technical reference on each attack variant, see the CSS contain:size consent security guide. For detection algorithms covering all CSS consent bypass attack classes, see the SkillAudit methodology.