MCP server CSS contain: layout security: independent formatting context, float isolation, and consent clipping attacks
Published 2026-09-26 — SkillAudit Research
CSS contain: layout declares that the element's layout is independent of the rest of the page. The browser may optimize rendering by assuming nothing outside the element affects its internal layout, and nothing inside affects the layout of things outside. This is a genuine performance optimization for complex components — but it also creates an isolation boundary that MCP servers can exploit to prevent host-page layout corrections from reaching consent text.
The key characteristic of layout containment is that it establishes a new formatting context. Floats, absolutely positioned descendants, and margin collapse all behave as if the element is the root of an independent document subtree. Combined with explicit heights and overflow: hidden, this creates a consent container that clips text in a way the host page cannot correct, because the host's layout adjustments do not cross the containment boundary.
Containment interaction with overflow: When contain: layout is combined with overflow: hidden and a fixed height, the consent container becomes a sealed box. Internal content that overflows is clipped. External positioned elements cannot reach inside. The host cannot expand the box by adjusting ancestor layout. The consent text is trapped.
Attack findings
Setting
contain: layout with a fixed height and overflow: hidden creates a sealed consent container. The acceptance clause — the last line of consent text — is positioned at the bottom of the natural document flow. If the fixed height is set to exactly the height of the permission list, the acceptance clause overflows below the height boundary and is clipped by overflow: hidden. Host-page layout cannot correct this because contain: layout prevents ancestor layout from expanding the box. Static audits may report the correct consent text in the DOM without detecting the clipping.
.consent-dialog {
contain: layout; /* independent formatting context */
height: 180px; /* sized to show permission list only */
overflow: hidden;
}
/* DOM structure:
scrollHeight: 240px
clientHeight: 180px
No scrollbar (overflow:hidden)
Acceptance clause: not rendered */
Within a layout-contained element, floats from outside cannot affect internal layout. But the MCP server controls the internal DOM. It can inject floated non-consent elements inside the contain boundary that push the consent block below the fixed height clip. Because
contain: layout prevents the containing block from growing to accommodate the float, the float + consent content stack taller than the fixed height. The acceptance clause at the bottom of the stack is clipped. No external layout adjustment can un-float the injected elements.
.consent-dialog {
contain: layout;
height: 200px;
overflow: hidden;
}
/* Inside the contained element: */
.consent-dialog > .mcp-injected-float {
float: right;
width: 60px;
height: 120px; /* large float pushes consent text down */
}
/* The float does not clear outside the containment boundary.
The consent text wraps around the float and extends below 200px.
Acceptance clause falls below the height clip.
Removing the float via host CSS: blocked by containment isolation. */
contain: layout establishes a new containing block for absolutely positioned descendants (equivalent to position: relative without actually setting position). An MCP server can set the consent text as an absolutely positioned child within the contain boundary with top and left values that position it outside the visible area, while static analysis confirms the text exists in the DOM and is position: absolute — which is a common and unremarkable layout choice. The text is positioned at e.g. top: 300px inside a 200px contained box, making it invisible.
.consent-dialog {
contain: layout; /* establishes containing block for abs. children */
height: 200px;
overflow: hidden;
}
.consent-text {
position: absolute;
top: 250px; /* below the 200px height — clipped by overflow:hidden */
left: 0;
}
/* Unlike a normally positioned dialog, there is no ancestor
with position:relative that the host can target to move this.
The contain:layout element IS the containing block.
The text is present in DOM, position:absolute is unremarkable,
but it renders below the clip boundary. */
Layout containment prevents margin collapse between the contained element and its children. In normal document flow, the top margin of the first child of a block collapses with the parent's top margin, so a consent element with
margin-top: 40px might not actually push down — the margins collapse. Inside a layout-contained element, no such collapse occurs. An MCP server can rely on this to size the fixed-height container based on the non-collapsed child margin calculation, causing the last consent element to extend beyond the height boundary due to the additional un-collapsed margin. Reviewers computing the height manually based on standard margin collapse rules will calculate a shorter total and miss the overflow.
.consent-dialog {
contain: layout;
height: 180px; /* sized assuming margin collapse — but collapse blocked */
overflow: hidden;
}
.consent-dialog > p:first-child {
margin-top: 24px; /* does NOT collapse with parent margin — adds 24px */
}
/* Without contain:layout, margin collapse would absorb the 24px.
With contain:layout, the 24px is real offset.
Total content height = 180px + 24px unintended extra.
Acceptance clause overflows by 24px. Auditor computing height assumes collapse. */
Detection
function checkContainLayout(el) {
const cs = getComputedStyle(el);
const containValue = cs.contain || '';
/* contain: layout, contain: strict, contain: content all include layout */
const hasLayoutContain = containValue.split(/\s+/).some(v =>
v === 'layout' || v === 'strict' || v === 'content'
);
if (!hasLayoutContain) return null;
const findings = [];
/* Check for clipping + fixed height combination */
if ((cs.overflow === 'hidden' || cs.overflow === 'clip') &&
cs.height !== 'auto') {
const sh = el.scrollHeight;
const ch = el.clientHeight;
if (sh > ch) {
findings.push({
severity: 'high',
issue: `contain:layout + overflow:hidden + fixed height clips ${sh - ch}px of consent content; scrollHeight:${sh}px > clientHeight:${ch}px`
});
}
}
/* Check for absolutely positioned children outside visible area */
for (const child of el.querySelectorAll('[style*="absolute"], [style*="position"]')) {
const childCs = getComputedStyle(child);
if (childCs.position === 'absolute') {
const childRect = child.getBoundingClientRect();
const parentRect = el.getBoundingClientRect();
if (childRect.top > parentRect.bottom || childRect.bottom < parentRect.top) {
findings.push({
severity: 'medium',
issue: 'contain:layout establishes containing block for absolutely positioned consent child positioned outside visible area'
});
}
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Check for contain property values that include layout (layout, strict, content) on consent containers | Layout containment changes how floats, absolute children, and margin collapse interact with the consent element — all of which can be exploited to clip text |
Compare scrollHeight to clientHeight when overflow: hidden is present | Clipped content does not produce a scrollbar with overflow:hidden; scrollHeight>clientHeight reveals hidden content regardless of whether contain:layout is the cause |
| Audit absolutely positioned children within layout-contained elements | contain: layout silently establishes a positioning context — absolutely positioned consent children may be intentionally placed outside the visible area |
| Account for blocked margin collapse when computing expected element height | Layout containment prevents margin collapse; height calculations that assume collapse will underestimate total content height inside the containment boundary |
SkillAudit detects layout containment on consent elements and checks for clipped overflow, out-of-bounds absolutely positioned children, and blocked margin collapse height miscalculations. Run a free audit on any MCP server GitHub URL.