Security reference · CSS injection · Flexbox attacks · Flex item sizing
MCP server CSS flex-basis security
CSS flex-basis defines the initial main-size of a flex item before any space distribution. It works together with flex-grow (how much of spare space the item claims) and flex-shrink (how much the item yields when the container is too small). MCP servers exploit these three values together to collapse consent disclosures to zero, push them off the flex line entirely, or force them into overflow-clipped areas — all without using display:none or visibility:hidden.
flex-basis collapse attack surface
| Attack configuration | flex-basis | Supporting properties | Effect on consent |
|---|---|---|---|
| Zero-collapse | 0 | flex-shrink: 1; overflow: hidden | Consent collapses to 0 main-size when flex container is at minimum size; text clipped to zero width |
| Oversized push-off | 200% | flex-wrap: nowrap on container | Consent item wider than the entire flex line; the install button item is at 0% or auto; consent overflows off the flex line |
| Auto-zero inherit | auto | width: 0; overflow: hidden on item | flex-basis: auto reads the item's width; with width:0 the flex-basis resolves to zero |
| Viewport-unit overflow | 100vh | Fixed-height container with overflow: hidden | Consent item's cross-axis size becomes 100vh; in a column flex container with fixed height, item is clipped |
Flex-basis attacks require the right flex-direction: In a flex-direction: row container, flex-basis controls width. In a flex-direction: column container, flex-basis controls height. Attacks that collapse width require row direction; attacks that collapse height require column direction. MCP must pair the flex-basis value with the correct container direction, but both are common layout patterns that do not raise suspicion.
Attack 1: flex-basis: 0 with flex-shrink — zero-width collapse
When flex-basis: 0 is set on a flex item, its hypothetical main size is zero before flex-shrink is applied. In a constrained flex container where the total flex-basis of all items exceeds the container width, each item that has flex-shrink active yields space. If the install form item has flex-grow: 1 and the consent item has flex-basis: 0; flex-shrink: 1; flex-grow: 0, the algorithm assigns all available space to the install form and squeezes consent to zero:
/* Malicious CSS — SA-CSS-FLEXBASIS-001 */
/* Row flex container — flex-basis controls width */
.mcp-install-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%; /* or fixed pixel width */
overflow: hidden; /* clips items that collapse below their minimum content size */
}
.mcp-install-form {
flex: 1 1 auto; /* grow:1, shrink:1, basis:auto — takes all available space */
}
.mcp-consent-disclosure {
flex: 0 1 0; /* grow:0 (don't claim space), shrink:1 (yield all), basis:0 */
overflow: hidden;
/* The flex item starts at 0 width (basis:0)
No extra space is distributed (flex-grow:0 means it gets none of the free space)
flex-shrink:1 means it yields further if the container is under pressure
Result: consent item rendered at 0px width — text clipped by overflow:hidden */
min-width: 0; /* required: without this, min-content width prevents full collapse */
}
/* Why min-width matters:
By default, flex items have min-width: auto, which means they will be at least
as wide as their minimum content (longest unbreakable word). Setting min-width:0
allows the item to shrink below its content minimum — enabling full zero-collapse.
The min-width:0 is a common flex fix pattern, not suspicious in context. */
/* Detection: check for flex items with consent text at zero or near-zero width */
function detectFlexBasisZeroCollapseAttack() {
const findings = [];
const candidates = document.querySelectorAll('*');
for (const el of candidates) {
if (!/consent|disclosure|terms|privacy|by installing|by clicking/i.test(el.textContent || '')) continue;
if (el.children.length > 5) continue;
const style = getComputedStyle(el);
if (style.display === 'none' || style.visibility === 'hidden') continue;
const flexBasis = style.flexBasis;
const flexShrink = parseFloat(style.flexShrink);
const rect = el.getBoundingClientRect();
if ((flexBasis === '0px' || flexBasis === '0') && flexShrink > 0 && rect.width < 10) {
findings.push({ id: 'SA-CSS-FLEXBASIS-001', severity: 'critical',
message: `Consent flex item has flex-basis:0 + flex-shrink:${flexShrink}; rendered width is ${Math.round(rect.width)}px — zero-collapse attack. min-width: ${style.minWidth}` });
}
}
return findings;
}
The flex-basis:0 shorthand hides in flex: 0 1 0: The CSS shorthand flex: 0 1 0 sets grow, shrink, and basis in one declaration. Reading it as a plain number sequence, "0 1 0" looks like an unusual but benign flex specification — only knowledge of the flex shorthand's third value being flex-basis reveals the zero-basis attack. Many code reviewers recognize flex: 1 (common) but may not parse flex: 0 1 0 (uncommon) at a glance.
Attack 2: flex-basis: 200% with nowrap — oversized push-off
Setting a consent item's flex-basis to a percentage larger than 100% of the flex container creates an item that is intrinsically "too wide." With flex-wrap: nowrap on the container (no line wrapping), the flex algorithm attempts to fit all items on one line by applying flex-shrink. If the install button has flex-shrink: 0 (won't shrink) and the consent has a very high flex-basis, the algorithm shrinks consent aggressively — or the consent overflows if flex-shrink: 0 is also set, pushing it off-screen:
/* Malicious CSS — SA-CSS-FLEXBASIS-002 */
/* Column container: install form items stacked vertically */
/* flex-basis: 100vh forces the consent item to be viewport-height tall */
.mcp-install-col {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
height: 300px; /* fixed container height */
overflow: hidden; /* clips items exceeding the 300px */
}
.mcp-install-form {
flex: 0 0 auto; /* form takes its natural height, doesn't shrink */
}
.mcp-install-button {
flex: 0 0 auto; /* button takes its natural height */
}
.mcp-consent-disclosure {
flex: 0 0 100vh; /* flex-basis: 100vh — item wants to be 1 viewport tall */
/* grow:0 (no extra space), shrink:0 (won't shrink) */
/* Result: consent is 100vh tall but container is only 300px */
/* overflow:hidden clips consent entirely — nothing painted */
}
/* Alternative: percentage-based basis in row direction */
.mcp-install-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 400px;
overflow: hidden;
}
.mcp-install-form {
flex: 0 0 400px; /* takes all 400px */
}
.mcp-consent-disclosure {
flex: 0 0 200%; /* wants to be 200% of 400px = 800px */
/* With nowrap and form taking 400px, consent goes from 400px to 400+800=1200px */
/* All 800px of consent is off-screen to the right */
/* overflow:hidden on container clips it */
}
/* Detection: check for consent flex items with viewport-unit or large percentage flex-basis */
function detectFlexBasisOversizedAttack() {
const findings = [];
const candidates = document.querySelectorAll('*');
for (const el of candidates) {
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
if (el.children.length > 5) continue;
const style = getComputedStyle(el);
/* Check if flex-basis is very large relative to viewport */
const flexBasisPx = parseFloat(style.flexBasis);
const vw = window.innerWidth;
const vh = window.innerHeight;
if (flexBasisPx > vw || flexBasisPx > vh) {
findings.push({ id: 'SA-CSS-FLEXBASIS-002', severity: 'high',
message: `Consent flex item has flex-basis:${Math.round(flexBasisPx)}px (larger than viewport ${vw}x${vh}) — oversized basis forces overflow. flex-shrink: ${style.flexShrink}` });
}
/* Check if rendered rect is outside bounds */
const rect = el.getBoundingClientRect();
if (rect.left > vw || rect.top > vh || rect.right < 0 || rect.bottom < 0) {
if (style.display !== 'none') {
findings.push({ id: 'SA-CSS-FLEXBASIS-002', severity: 'critical',
message: `Consent flex item rendered off-screen (${Math.round(rect.left)},${Math.round(rect.top)} to ${Math.round(rect.right)},${Math.round(rect.bottom)}) — flex-basis overflow attack` });
}
}
}
return findings;
}
Attack 3: flex-basis: auto + width: 0 — zero-width auto-inherit
When flex-basis is set to auto, it looks up the item's width (for row direction) or height (for column direction) as the basis value. This is the default behavior. MCP exploits this by setting width: 0 on the consent item — which is then inherited as the flex-basis — combined with overflow: hidden. The CSS reads as two separate non-suspicious properties; only understanding that flex-basis: auto inherits width: 0 reveals the zero-width collapse:
/* Malicious CSS — SA-CSS-FLEXBASIS-003 */
/* The attack uses two separate CSS properties that each look innocuous: */
.mcp-consent-disclosure {
/* Looks like an accidental or placeholder width setting: */
width: 0;
/* Looks like the default flex-basis (it is): */
flex-basis: auto; /* auto → resolves to width:0 → basis is 0 */
flex-shrink: 1;
overflow: hidden; /* clips the zero-width box */
min-width: 0; /* allows collapse below content minimum */
}
/* How the CSS cascade makes this hard to spot:
- width:0 may appear in a general reset or utility class: .w-0 { width: 0 }
- flex-basis:auto is the spec default — it appears in any flex normalization
- The combination is the attack: flex-basis:auto reads width:0 as its value
- No single property is suspicious; the interaction between them creates the attack
/* Common obfuscation: utility class composition */
/* The width:0 comes from a utility class applied to the consent element */
/* The flex-basis:auto is in the flex container's normalization stylesheet */
/* Neither class "looks like" a consent-hiding technique */
/* Variant: CSS custom property chain */
.mcp-consent-disclosure {
--consent-width: 0px;
width: var(--consent-width);
flex-basis: auto; /* resolves to var(--consent-width) = 0px */
}
/* Detection: check for flex items where flex-basis resolves to 0 via width inheritance */
function detectFlexBasisAutoZeroAttack() {
const findings = [];
const candidates = document.querySelectorAll('*');
for (const el of candidates) {
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
if (el.children.length > 5) continue;
const style = getComputedStyle(el);
/* flex-basis:auto plus zero width — check computed values */
if (style.flexBasis === 'auto' && style.width === '0px' && style.display !== 'none') {
const rect = el.getBoundingClientRect();
if (rect.width < 5) {
findings.push({ id: 'SA-CSS-FLEXBASIS-003', severity: 'high',
message: `Consent flex item has flex-basis:auto + width:0px — auto inherits zero width as flex-basis. Rendered width: ${Math.round(rect.width)}px. This is the auto-zero inherit attack pattern.` });
}
}
/* Also check the inline style attribute for width:0 */
const inlineStyle = el.getAttribute('style') || '';
if (/width\s*:\s*0/.test(inlineStyle) && style.flexBasis === 'auto') {
findings.push({ id: 'SA-CSS-FLEXBASIS-003', severity: 'high',
message: `Consent element has inline width:0 + flex-basis:auto — auto-zero inherit collapse. Inline: "${inlineStyle}"` });
}
}
return findings;
}
Attack 4: flex-basis pushing consent to second flex line with overflow-hidden wrap
With flex-wrap: wrap, flex items that don't fit on the first line wrap to a second line. If the consent item's total basis is just slightly larger than the remaining space on the first line, it wraps to line 2. With a container height constrained to fit only line 1 and overflow: hidden, line 2 (containing consent) is clipped. The install form occupies line 1 (visible); consent wraps to line 2 (hidden). The manipulation is subtle — consent is not oversized, just sized to not fit the remaining line space:
/* Malicious CSS — SA-CSS-FLEXBASIS-004 */
/* Two-item row flex, wrapping enabled, container height fits only one row */
.mcp-install-container {
display: flex;
flex-direction: row;
flex-wrap: wrap; /* allows items to wrap to second line */
align-content: flex-start; /* first line at top; second line below */
height: 80px; /* fits only one wrapped line (row height) */
overflow: hidden; /* clips the second wrapped line (containing consent) */
}
.mcp-install-form {
flex: 1 1 100%; /* takes full container width on line 1 */
/* height ~60px — fits within the 80px container */
}
.mcp-consent-disclosure {
flex: 1 1 100%; /* also claims full width — wraps to line 2 */
/* On line 2: starts at y = form height (60px) */
/* Line 2 is within 80px - 60px = 20px of the container... */
/* But consent has min-height from its content, wrapping it below 80px */
}
/* More reliable variant: force consent to second line with exact basis calculation */
.mcp-install-container {
display: flex;
flex-wrap: wrap;
width: 400px;
height: 50px; /* just enough for the install form line */
overflow: hidden;
}
.mcp-install-form {
flex-basis: 300px; /* takes 300px of 400px on first line */
}
.mcp-consent-disclosure {
flex-basis: 150px; /* 150px doesn't fit remaining 100px → wraps to line 2 */
/* Line 2 starts at y = first-line height → clipped by height:50px */
}
/* Why this is subtle:
- flex-wrap:wrap is common in responsive layouts
- The specific basis values (300px / 150px) appear to be layout choices
- overflow:hidden + height:50px appears to be a normal scroll or height constraint
- Only the interaction between the four values creates the consent-on-line-2 attack
/* Detection: check if consent items are on a wrapped flex line that overflows container */
function detectFlexWrapLineOverflowAttack() {
const findings = [];
const candidates = document.querySelectorAll('*');
for (const el of candidates) {
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
if (el.children.length > 5) continue;
const parent = el.parentElement;
if (!parent) continue;
const parentStyle = getComputedStyle(parent);
if (parentStyle.display !== 'flex' && parentStyle.display !== 'inline-flex') continue;
if (parentStyle.flexWrap !== 'wrap' && parentStyle.flexWrap !== 'wrap-reverse') continue;
if (parentStyle.overflow !== 'hidden' && parentStyle.overflowY !== 'hidden') continue;
const parentRect = parent.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
/* Check if consent element's top is beyond the parent's bottom */
if (elRect.top >= parentRect.bottom || elRect.bottom <= parentRect.top) {
findings.push({ id: 'SA-CSS-FLEXBASIS-004', severity: 'high',
message: `Consent element has wrapped to a flex line that is clipped by parent overflow:hidden. Parent height: ${Math.round(parentRect.height)}px. Consent rendered at y: ${Math.round(elRect.top - parentRect.top)}px (outside visible area). flex-basis: ${getComputedStyle(el).flexBasis}` });
}
}
return findings;
}
min-width: 0 is a flex performance pattern, not a red flag alone: Setting min-width: 0 on flex items is a common fix for flex overflow issues — it allows items to shrink below their content minimum. This property appears frequently in legitimate flex layouts and CSS resets. SkillAudit does not flag min-width: 0 alone; it flags min-width: 0 combined with flex-basis: 0 and near-zero rendered width on consent-containing elements, which is the signature of a deliberate zero-collapse attack.
SkillAudit findings for CSS flex-basis consent attacks
flex: 0 1 0 (or flex-basis: 0; flex-shrink: 1; flex-grow: 0) combined with min-width: 0; overflow: hidden on consent element. Consent collapses to zero main-size. Rendered width is 0px. Display is block; visibility is visible. Detected by checking flexBasis === '0px' with flexShrink > 0 and getBoundingClientRect().width < 5.flex-basis: 100vh (or other viewport-unit basis) on consent item in a fixed-height column container with overflow: hidden. Consent item is intrinsically taller than the container; overflow clipping removes it from view. Detected by comparing computed flexBasis in pixels against viewport dimensions.flex-basis: auto with width: 0 on consent element. The auto basis resolves to the element's width; with width: 0, the flex-basis is zero. Combined with overflow: hidden, consent collapses. Attack is split across two non-suspicious CSS properties. Detected by checking flexBasis === 'auto' and width === '0px' together.flex-wrap: wrap container with fixed height and overflow: hidden. Second line is clipped. Install form on first line remains visible. Detected by checking if consent element's bounding rect falls below the parent flex container's visible bounds.Related MCP consent attack research
- CSS flex shorthand attacks — flex-grow and flex-shrink manipulation
- CSS gap attacks — flex row-gap and column-gap consent displacement
- CSS place-content attacks — flex/grid cross-axis consent hiding
- CSS overflow attacks — consent clipping via overflow:hidden
- CSS Pseudo-Class Attacks on MCP Consent (overview)
Audit your MCP server for flex-basis consent collapse attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-FLEXBASIS findings.