Security reference · CSS injection · Flexbox attacks · Space distribution
MCP server CSS flex-grow security
CSS flex-grow determines how much of the positive free space in a flex container a given item claims. It works as a ratio: an item with flex-grow: 2 gets twice as much free space as one with flex-grow: 1. An item with flex-grow: 0 gets none. MCP servers exploit this to keep consent at its initial flex-basis (often zero) while the install form expands to fill all available space — or to inject a high-grow spacer that absorbs all free space before consent can claim any.
flex-grow attack surface
| Attack configuration | flex-grow (consent) | Supporting properties | Effect on consent |
|---|---|---|---|
| Zero-grow lock | 0 | flex-basis: 0; min-width: 0; overflow: hidden | Consent stays at flex-basis (zero); all positive free space goes to install form with flex-grow:1; consent rendered at 0px width |
| Spacer absorption | 1 | Spacer element with flex-grow: 999 inserted between form and consent | Spacer takes 999/1000 of free space; consent claims 1/1000 — renders as 1-2px |
| JS-toggled collapse | 1 initially → 0 | Class toggle on .mcp-loaded changes grow from 1 to 0 | Consent visible at load time (passes audit); hidden after MCP script runs |
| Pre-consumed space | 1 | An invisible flex-grow: 1 sibling with height: 0; overflow: hidden — zero visible but full grow factor | Invisible sibling splits grow factor 50/50; consent gets half the expected space |
flex-grow only distributes free space: If the flex container has no free space (all items' flex-basis values already fill it), flex-grow does nothing. Attacks that combine flex-grow: 0 on consent with flex-basis: 0 are effective because they ensure consent starts at zero and can never grow. If the container did have free space and consent had flex-grow: 0, consent would remain visible at its basis size — the zero-basis is the required accomplice.
Attack 1: flex-grow: 0 with zero-basis lock — consent can never expand
The core zero-grow attack pairs flex-grow: 0 on consent with flex-basis: 0. Without any positive free space allocation and starting at zero, the consent item's computed main-size is zero. The install form item uses flex: 1 1 auto, which means it grows to fill all remaining space. With overflow: hidden on both the consent item and the container, the zero-width consent text is clipped entirely:
/* Malicious CSS — SA-CSS-FLEXGROW-001 */
.mcp-install-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
overflow: hidden;
width: 100%;
}
.mcp-install-form {
flex: 1 1 auto; /* grow:1 — expands to fill all positive free space */
}
.mcp-consent-disclosure {
flex: 0 1 0; /* grow:0 — never claims positive free space; basis:0; shrink:1 */
min-width: 0; /* removes the min-content floor — enables full zero collapse */
overflow: hidden;
}
/* The shorthand flex: 0 1 0 reads as: grow=0, shrink=1, basis=0.
Combined with min-width:0, this is the canonical flex collapse attack.
flex-grow:0 alone would not hide consent if flex-basis were auto or positive.
The partnership of grow:0 + basis:0 + min-width:0 creates the zero-size. */
/* Detection */
function detectFlexGrowZeroLock() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
if (el.children.length > 5) continue;
const s = getComputedStyle(el);
if (s.display === 'none') continue;
const grow = parseFloat(s.flexGrow);
const basis = s.flexBasis;
const rect = el.getBoundingClientRect();
if (grow === 0 && (basis === '0px' || basis === '0') && rect.width < 5) {
findings.push({ id: 'SA-CSS-FLEXGROW-001', severity: 'critical',
message: `Consent flex item has flex-grow:0 + flex-basis:0; rendered width is ${Math.round(rect.width)}px — zero-grow lock attack. min-width:${s.minWidth}` });
}
}
return findings;
}
flex: 0 reads as flex-grow: 0, flex-shrink: 1, flex-basis: 0%: The shorthand flex: 0 (single value) in CSS sets grow to 0, shrink to 1, and basis to 0%. This is different from flex: 0 0 auto. The compact form flex: 0 is a frequently used shorthand in component libraries and may appear alongside legitimate content — which is exactly why MCP uses it on consent elements.
Attack 2: flex-grow: 999 spacer element — absorbs all free space before consent
Rather than manipulating the consent element's own flex-grow, this attack inserts an invisible spacer element between the install form and the consent disclosure. The spacer has flex-grow: 999 and zero visible dimensions (height: 0; overflow: hidden). In the space distribution algorithm, the spacer claims 999 parts of every 1000 parts of free space. Consent gets 1 part — rendered as a pixel-wide sliver. The spacer itself is visually empty, so users see nothing but the install form:
/* Malicious CSS — SA-CSS-FLEXGROW-002 */
.mcp-install-container {
display: flex;
flex-direction: column;
height: 400px;
overflow: hidden;
}
.mcp-install-form {
flex: 0 0 auto; /* natural height, no grow */
}
.mcp-install-spacer {
flex-grow: 999; /* absorbs 99.9% of remaining space */
height: 0; /* spacer content is zero height */
overflow: hidden;
/* visually: nothing. auditor: nothing. algorithm: takes all space */
}
.mcp-consent-disclosure {
flex-grow: 1; /* gets 1/1000 of free space */
overflow: hidden;
/* at 400px container with 100px form: 300px free space
spacer gets 299.7px; consent gets 0.3px → rendered as 1px → text clipped */
}
/* Detection: check for flex items with extremely high flex-grow near consent */
function detectFlexGrowSpacerAttack() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
const children = [...el.children];
const highGrowers = children.filter(c => parseFloat(getComputedStyle(c).flexGrow) > 50);
const consentSiblings = children.filter(c =>
/consent|disclosure|terms|privacy/i.test(c.textContent || '')
);
if (highGrowers.length > 0 && consentSiblings.length > 0) {
findings.push({ id: 'SA-CSS-FLEXGROW-002', severity: 'high',
message: `Flex container has high-grow sibling (flex-grow:${parseFloat(getComputedStyle(highGrowers[0]).flexGrow)}) near consent element — spacer absorption attack suspected.` });
}
}
return findings;
}
Attack 3: JS-toggled flex-grow — visible at audit time, hidden after load
This timing attack relies on consent being visible during any static or load-time audit, then hidden after MCP's JavaScript runs. Consent starts with flex-grow: 1 — it participates normally in space distribution and is visible. After MCP loads (via a class toggle, attribute set, or direct style manipulation), the consent element's flex-grow is set to 0, and simultaneously its flex-basis is set to 0. The transition can be instant (imperceptible to users) or delayed to avoid detection:
/* Malicious CSS — SA-CSS-FLEXGROW-003 */
/* Load-time: consent is visible (flex-grow:1, flex-basis:auto) */
.mcp-consent-disclosure {
flex: 1 1 auto; /* normal — visible at load */
}
/* After MCP loads: .mcp-loaded class added to body */
.mcp-loaded .mcp-consent-disclosure {
flex: 0 1 0; /* grow:0, basis:0 — collapses consent */
min-width: 0;
overflow: hidden;
}
/* JavaScript timing */
document.addEventListener('DOMContentLoaded', () => {
/* Delay to ensure load-time auditors have already run */
requestAnimationFrame(() => {
requestAnimationFrame(() => {
document.body.classList.add('mcp-loaded');
});
});
});
/* Detection: check for CSS rules where .mcp-loaded (or similar class) changes flex-grow */
function detectFlexGrowJSToggle() {
const findings = [];
const sheets = [...document.styleSheets];
for (const sheet of sheets) {
let rules;
try { rules = [...sheet.cssRules]; } catch { continue; }
for (const rule of rules) {
if (rule.type !== 1) continue; /* CSSStyleRule only */
const selector = rule.selectorText || '';
const flexGrow = rule.style.flexGrow || rule.style.flex;
/* Flag rules where a class selector changes flex-grow to 0 on consent elements */
if (/\.mcp-|\.loaded|\.ready|\.installed/i.test(selector) &&
(rule.style.flexGrow === '0' || /^0\s/.test(rule.style.flex))) {
findings.push({ id: 'SA-CSS-FLEXGROW-003', severity: 'critical',
message: `CSS rule "${selector}" sets flex-grow:0 or flex:0 — JS-triggered flex-grow collapse suspected. flex value: "${flexGrow}"` });
}
}
}
return findings;
}
requestAnimationFrame double-tap defeats rAF-based auditors: Some security scanners check layout after a single requestAnimationFrame callback to allow for JS initialization. This attack uses two nested requestAnimationFrame calls, which execute in the second animation frame — after any single-rAF auditor has already completed its check. The consent is visible when the auditor runs; it collapses in the next frame.
Attack 4: invisible flex-grow sibling pre-consumes space
This variant inserts a sibling element that has flex-grow: 1 but is rendered at zero visible size via font-size: 0; height: 0; padding: 0. Because both the hidden sibling and the consent element share flex-grow: 1, they split available space 50/50. Consent gets only half the free space it would otherwise claim. In a column flex container where the install form takes most of the fixed height, this halved allocation can push consent below the overflow boundary. The invisible sibling is not a spacer (flex-grow:999) — it has a plausible grow ratio — making it harder to flag by ratio scanning:
/* Malicious CSS — SA-CSS-FLEXGROW-004 */
.mcp-install-container {
display: flex;
flex-direction: column;
height: 320px;
overflow: hidden;
}
.mcp-install-form { flex: 0 0 auto; } /* 200px natural height */
.mcp-install-button { flex: 0 0 auto; } /* 40px natural height */
/* Free space = 320 - 200 - 40 = 80px */
.mcp-ghost-spacer {
flex-grow: 1; /* takes 40px of free space */
height: 0; /* zero content height */
font-size: 0; /* no text visible */
overflow: hidden; /* can't overflow */
/* Looks like: a flex normalizer or placeholder. */
}
.mcp-consent-disclosure {
flex-grow: 1; /* also takes 40px of free space */
/* Total height: 40px. At 14px font, fits ~2-3 lines.
But overflow:hidden is set on the container;
if the 40px is just under the minimum required for consent text,
the first line may be cut by the container's 320px boundary. */
}
/* Detection */
function detectFlexGrowPreConsumptionAttack() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
const children = [...el.children];
const invisible = children.filter(c => {
const cs = getComputedStyle(c);
const rect = c.getBoundingClientRect();
return parseFloat(cs.flexGrow) > 0
&& rect.height < 2
&& !/consent|disclosure|terms/i.test(c.textContent || '');
});
if (invisible.length > 0) {
const consentKids = children.filter(c =>
/consent|disclosure|terms|privacy/i.test(c.textContent || '')
);
if (consentKids.length > 0) {
findings.push({ id: 'SA-CSS-FLEXGROW-004', severity: 'high',
message: `Flex container has ${invisible.length} invisible flex-grow sibling(s) near consent — pre-consumption attack suspected.` });
}
}
}
return findings;
}
SkillAudit findings for CSS flex-grow consent attacks
flex-grow: 0 combined with flex-basis: 0 and min-width: 0; overflow: hidden on consent element. Consent cannot grow from its zero basis. All free space goes to the install form. Rendered width is 0px. Detected by checking flexGrow === '0' with flexBasis === '0px' and getBoundingClientRect().width < 5.flex-grow > 50 that is zero-height or zero-width (visually invisible) positioned as a sibling to a consent-containing element. Spacer absorbs nearly all free space; consent receives <1% of available space. Detected by scanning flex container children for high-grow invisible siblings near consent elements..mcp-loaded, .installed) to set flex-grow: 0 on a consent-containing element. Load-time auditors see consent with flex-grow: 1 (visible); post-JS-execution auditors see flex-grow: 0 (collapsed). Detected by scanning all CSS rules for class-triggered flex-grow changes on consent selectors.flex-grow > 0 that pre-consume available space, leaving consent with less allocated height than it needs to display fully. Detected by checking for invisible positive-grow siblings in the same flex container as consent elements.Related MCP consent attack research
- CSS flex-basis attacks — flex item collapse and overflow hiding
- CSS flex-shrink attacks — aggressive shrink factor consent collapse
- CSS gap attacks — row-gap and column-gap consent displacement
- CSS overflow attacks — consent clipping via overflow:hidden
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
Audit your MCP server for flex-grow consent collapse attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-FLEXGROW findings.