MCP server CSS contain:strict consent security
CSS containment (contain) was designed to improve browser rendering performance by declaring that an element's subtree is independent from the rest of the document. contain:strict applies all four containment types simultaneously: size (collapses intrinsic size to zero without an explicit size), layout (creates a new formatting context), style (prevents CSS counters and quotes escaping the subtree), and paint (creates a new stacking context and clips painting to the element's border box). Applied to a consent container, contain:strict without an explicit size makes the container disappear entirely — it collapses to 0×0 pixels and its content is clipped to that zero-area paint region.
Attack findings
Background: CSS containment and its consent security implications
CSS containment (the contain property, introduced in Chrome 52, Firefox 69, Safari 15.4) provides performance optimizations by declaring layout, paint, and size independence for an element's subtree. The browser uses these declarations to skip unnecessary recalculation steps. For consent security, containment creates a set of side effects — stacking context creation, paint region clipping, intrinsic size collapse — that are exploitable when applied to consent containers. The attacks look like performance optimizations in code review: contain:strict is a standard recommendation for widgets and isolated components in the web performance community.
Audit gap: Static CSS analysis checking for display:none or visibility:hidden misses containment attacks entirely. The consent element has neither property; its visibility failure comes from paint clipping or intrinsic size collapse that require computed layout evaluation to detect.
Attack 1 — contain:strict collapses container to 0×0 (SA-CSS-CONT-001)
The contain:size value tells the browser to treat the element as if it had no content for sizing purposes — its intrinsic size becomes 0. Without an explicit width and height set, the container collapses to 0×0 pixels. The contain:paint value then clips all painting to the element's border box. A 0×0 border box has no paint region; all consent content is clipped. The DOM contains the consent text nodes, innerText returns the consent text, but the element renders zero visible pixels.
/* Attack: contain:strict collapses consent container to 0×0 */
.consent-wrapper {
contain: strict; /* = contain: size layout style paint */
/* No width or height set — contain:size treats intrinsic size as 0 */
/* contain:paint clips to 0×0 border box */
/* DOM: text nodes present; innerText: consent text; clientWidth: 0 */
}
/* Detection */
function checkContainmentCollapse(el) {
const cs = getComputedStyle(el);
const contain = cs.contain || '';
if (contain.includes('strict') || (contain.includes('size') && contain.includes('paint'))) {
const rect = el.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
return { vuln: 'SA-CSS-CONT-001',
detail: `contain:${contain} collapses consent to ${rect.width}×${rect.height}px` };
}
}
return null;
}
SA-CSS-CONT-001 (High). Detection requires checking both the computed contain value and the element's rendered dimensions. getBoundingClientRect() returns 0×0 for a collapsed contained element despite the DOM text being present. SkillAudit checks all consent elements for containment-induced dimension collapse.
Attack 2 — contain:paint creates stacking context for z-index override (SA-CSS-CONT-002)
contain:paint creates a new stacking context, similar to isolation:isolate or transform:translateZ(0). Within this stacking context, z-index comparisons are made relative to siblings within the same stacking context root. An element with z-index:0 that was previously painting below the consent container (because it shared a parent stacking context with higher-z-index siblings) now paints over it if the consent container has contain:paint and is in a lower-z-index position. The overlay does not need to change its own z-index; only the consent container's containment changes, altering the stacking order resolution.
/* Attack: contain:paint creates stacking context — changes overlay z-order */
.install-section {
contain: paint; /* New stacking context */
position: relative;
z-index: 1; /* Section-level z-index */
}
.decorative-card {
position: absolute;
z-index: 2; /* Previously: same stacking context as consent — valid order */
/* Now: paint stacking context changes comparison root */
background: rgba(255,255,255,0.98);
inset: 0;
}
.consent-text { position: relative; z-index: 1; }
/* Within install-section stacking context: decorative-card (z:2) > consent-text (z:1) */
/* Consent covered by white card */
Attack 3 — contain:layout traps absolute-positioned consent (SA-CSS-CONT-003)
contain:layout creates a new block formatting context (BFC), similar to overflow:hidden. Absolutely-positioned elements inside a contain:layout container position relative to that container as their containing block. This is the same behavior as position:relative on the container — but unlike position:relative, contain:layout is less obvious in a code review. The attack uses contain:layout on a zero-height section, then places consent inside it as position:absolute; top:0. The consent's absolute position is relative to the zero-height container; it renders at top:0 of a 0-height box — effectively invisible at the section's position in the document.
/* Attack: contain:layout zero-height container traps consent */
.install-section {
contain: layout; /* BFC — absolutely positioned children reference this container */
height: 0; /* Zero height — contained layout contributes 0px to document flow */
overflow: hidden; /* Clips absolute children outside 0-height bounds */
}
.consent-text {
position: absolute;
top: 0; left: 0;
/* Renders at top of 0-height container — clipped to 0px height */
/* DOM position: correct; offsetTop: valid; visible: 0px */
}
Attack 4 — contain:size + height:1px clips consent to 1px (SA-CSS-CONT-004)
Unlike the zero-size collapse in attack 1, this variant sets an explicit height to force a specific paint clip height. contain:size respects explicit width/height values; the explicit value is used as the intrinsic size. Setting height:1px with contain:size and contain:paint clips all content to a 1px-tall paint region. The consent text renders to a 1px sliver — technically non-zero, but unreadable. The scrollHeight would normally reveal the overflow, but contain:size prevents the element from reporting its content size to the layout engine; scrollHeight also returns the contained height (1px), not the content height. This makes the standard scrollHeight > clientHeight overflow detection ineffective.
/* Attack: contain:size + explicit 1px clips consent; scrollHeight also 1px */
.consent-wrapper {
contain: size paint;
height: 1px; /* Explicit: contain:size respects this value */
overflow: hidden; /* Clip backup for browsers where contain:paint insufficient */
}
/* clientHeight: 1; scrollHeight: 1 (contain:size prevents content-based scroll reporting)
getBoundingClientRect().height: 1
innerText: full consent text — DOM audit: PASS
Visual: 1px sliver — unreadable */
/* Detection: check contain value + clientHeight */
function checkContainSizeClip(el) {
const cs = getComputedStyle(el);
const contain = cs.contain || '';
if (contain.includes('size') || contain.includes('strict')) {
const h = el.getBoundingClientRect().height;
const textLength = (el.textContent || '').trim().length;
if (textLength > 20 && h < 20) {
return { vuln: 'SA-CSS-CONT-004',
detail: `contain:size — consent ${textLength} chars rendered in ${h}px height` };
}
}
return null;
}
SkillAudit detection: SkillAudit evaluates the computed contain value for all consent element ancestors and the consent element itself, checks rendered dimensions against content length, and identifies stacking context changes from contain:paint. Run a free audit →
Detection summary
| Attack ID | contain value + mechanism | Key detection signal |
|---|---|---|
| SA-CSS-CONT-001 | contain:strict → 0×0 size collapse | getBoundingClientRect() = 0×0 with DOM text present |
| SA-CSS-CONT-002 | contain:paint → stacking context z-index change | Consent ancestor has contain:paint; sibling overlay paints over consent in computed layer order |
| SA-CSS-CONT-003 | contain:layout zero-height → absolute consent trapped | Consent offsetTop=0 inside 0-height contain:layout container |
| SA-CSS-CONT-004 | contain:size + height:1px → scrollHeight bypass | clientHeight=1 with long textContent; scrollHeight also 1 (not diagnostic) |