Security reference · CSS injection · Flexbox attacks · Shrink factor manipulation
MCP server CSS flex-shrink security
CSS flex-shrink controls how much a flex item yields when the total flex-basis of all items exceeds the container size. The shrink algorithm distributes negative free space in proportion to each item's flex-shrink × flex-basis product. MCP servers exploit this by assigning an extremely high flex-shrink value to the consent element (forcing it to absorb all negative space) while setting flex-shrink: 0 on the install form (preventing it from yielding any space). Combined with min-width: 0, consent collapses to zero rendered size.
flex-shrink attack surface
| Attack configuration | flex-shrink (consent) | Supporting properties | Effect on consent |
|---|---|---|---|
| Asymmetric shrink | 999 | Install form: flex-shrink: 0; container: overflow: hidden | Consent absorbs all negative free space; collapses to near-zero while form holds its size |
| Min-width floor removal | 1 | min-width: 0; overflow: hidden | Default shrink:1 + min-width:0 collapses consent below its content minimum in a tight container |
| Asymmetric basis amplified | 1 | Consent: flex-basis: 300px; install form: flex-basis: 300px; flex-shrink: 0; container: width: 300px | Total basis 600px in 300px container = 300px negative space; form shrink-factor:0 means consent absorbs all 300px → collapses to 0 |
| JS-triggered shrink increase | 1 initially → 999 | Install button click triggers class change that increases flex-shrink | Consent visible before click; collapses at the moment user presses install button — user never reads consent |
The flex shrink algorithm uses weighted ratios: Negative free space is distributed in proportion to each item's flex-shrink × flex-basis product (the "scaled flex shrink factor"). An item with flex-shrink: 999 and flex-basis: 100px has a scaled factor of 99,900. An item with flex-shrink: 1 and flex-basis: 100px has a scaled factor of 100. The first absorbs 99,900/100,000 = 99.9% of negative space. Understanding the weighted formula is essential for detecting attacks that use basis-amplified ratios.
Attack 1: flex-shrink: 999 asymmetric — consent absorbs all negative space
The canonical asymmetric shrink attack sets an extreme shrink factor on the consent element (999) while protecting the install form with flex-shrink: 0. The container is undersized relative to the total flex-basis, creating negative free space. The flex shrink algorithm sees only the consent item participating (form has shrink-factor:0) and assigns all negative space to it. Combined with min-width: 0 and overflow: hidden, consent collapses to zero rendered width:
/* Malicious CSS — SA-CSS-FLEXSHRINK-001 */
.mcp-install-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 400px; /* fixed container */
overflow: hidden;
}
.mcp-install-form {
flex: 0 0 400px; /* flex-basis: 400px; flex-shrink: 0 — never yields */
}
.mcp-consent-disclosure {
flex: 0 999 100px; /* flex-basis: 100px; flex-shrink: 999; flex-grow: 0 */
min-width: 0; /* removes content minimum floor */
overflow: hidden;
/* Total basis: 400 + 100 = 500px in 400px container
Negative space: -100px
Form's scaled shrink factor: 400 × 0 = 0
Consent's scaled shrink factor: 100 × 999 = 99,900
Consent absorbs: 99,900/99,900 × 100px = 100px of shrinkage
Consent rendered width: 100 - 100 = 0px — full collapse */
}
/* Detection */
function detectFlexShrinkAsymmetric() {
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 shrink = parseFloat(s.flexShrink);
const rect = el.getBoundingClientRect();
if (shrink > 10 && rect.width < 5) {
findings.push({ id: 'SA-CSS-FLEXSHRINK-001', severity: 'critical',
message: `Consent flex item has flex-shrink:${shrink}; rendered width is ${Math.round(rect.width)}px — asymmetric shrink attack. min-width:${s.minWidth}` });
}
}
return findings;
}
flex-shrink: 0 on the install form is the other half of the attack: Without flex-shrink: 0 on the install form, negative space would be distributed proportionally between both items. The form would shrink a little; consent would shrink a little. With flex-shrink: 0 on the form, all negative space is forced onto consent. Auditors should check the siblings of consent-containing flex items for flex-shrink: 0 values — the asymmetry is the attack signature.
Attack 2: default flex-shrink: 1 with min-width: 0 — sub-content-minimum collapse
This attack uses the CSS default flex-shrink: 1 and does not set an extreme value. The attack works because the default min-width: auto for flex items prevents them from collapsing below their minimum content size. By overriding this to min-width: 0, the content minimum floor is removed. In a tight container where the install form takes most available space, consent shrinks past the minimum content size until it reaches zero:
/* Malicious CSS — SA-CSS-FLEXSHRINK-002 */
.mcp-install-row {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 200px; /* tight container */
overflow: hidden;
}
.mcp-install-form {
flex: 0 1 auto; /* shrink:1, basis:auto — will shrink */
min-width: 150px; /* won't shrink below 150px — still large relative to consent */
}
.mcp-consent-disclosure {
flex: 1 1 auto; /* grow:1, shrink:1 */
min-width: 0; /* ATTACK: removes the minimum content width floor */
overflow: hidden;
/* Without min-width:0: consent minimum content = width of longest word (e.g. "disclosure" = ~70px)
With min-width:0: consent can shrink to 0px
Container 200px - form 150px = 50px for consent
But form has min-width:150px and total basis may exceed 200px
→ consent shrinks to whatever remains after form satisfies its constraints */
}
/* Detection */
function detectFlexShrinkMinWidthZero() {
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;
if (s.minWidth === '0px' && parseFloat(s.flexShrink) > 0) {
const rect = el.getBoundingClientRect();
if (rect.width < 20) {
findings.push({ id: 'SA-CSS-FLEXSHRINK-002', severity: 'high',
message: `Consent flex item has min-width:0 + flex-shrink:${s.flexShrink}; rendered width is ${Math.round(rect.width)}px — sub-content-minimum collapse. flex-basis:${s.flexBasis}` });
}
}
}
return findings;
}
Attack 3: basis-amplified shrink — large basis that is entirely shrunk away
The flex shrink formula weighs by basis. An item with flex-basis: 500px; flex-shrink: 1 has a scaled shrink factor of 500 × 1 = 500. An item with flex-basis: 10px; flex-shrink: 1 has a factor of 10 × 1 = 10. In a container where both items sum to more than the container width, the first absorbs 500/510 = 98% of negative space. By assigning a large basis to consent and a small basis to the install form (both with shrink:1), the consent element absorbs nearly all negative space — without any extreme flex-shrink value that might trigger ratio-based audits:
/* Malicious CSS — SA-CSS-FLEXSHRINK-003 */
/* Both items use default flex-shrink:1 — no extreme values */
.mcp-install-row {
display: flex;
flex-wrap: nowrap;
width: 300px;
overflow: hidden;
}
.mcp-install-form {
flex: 0 1 10px; /* basis:10px, shrink:1 — scaled factor: 10 */
}
.mcp-consent-disclosure {
flex: 0 1 500px; /* basis:500px, shrink:1 — scaled factor: 500 */
min-width: 0;
/* Total: 510px in 300px → 210px negative space
Form absorbs: (10/510) × 210 ≈ 4px → rendered at 6px
Consent absorbs: (500/510) × 210 ≈ 206px → rendered at 294px... wait.
Actually in this example consent would be rendered at 294px (visible).
The attack needs the container to be smaller:
Use width:10px for container or flex-basis:500px in a 5px container. */
}
/* More accurate attack: large-basis consent in very tight container */
.mcp-install-row-tight {
display: flex;
width: 10px; /* very tight — far less than any basis */
overflow: hidden;
}
.mcp-install-form-tight {
flex: 0 1 5px; /* scaled shrink: 5 × 1 = 5; absorbs 5/(5+500) = ~1% negative space */
}
.mcp-consent-disclosure-tight {
flex: 0 1 500px; /* scaled shrink: 500 × 1 = 500; absorbs 500/505 = ~99% negative space */
min-width: 0;
/* Total: 505px in 10px → 495px negative space
Consent absorbs 99% = ~490px → rendered at 500 - 490 = ~10px → tiny */
}
/* Detection: flag consent items with very large flex-basis relative to rendered size */
function detectBasisAmplifiedShrink() {
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 basisPx = parseFloat(s.flexBasis);
const shrink = parseFloat(s.flexShrink);
const rect = el.getBoundingClientRect();
if (basisPx > 100 && shrink > 0 && rect.width < basisPx * 0.1) {
findings.push({ id: 'SA-CSS-FLEXSHRINK-003', severity: 'high',
message: `Consent flex item has large flex-basis (${Math.round(basisPx)}px) but rendered at ${Math.round(rect.width)}px — basis-amplified shrink attack. ${Math.round((1 - rect.width / basisPx) * 100)}% of basis was shrunk away.` });
}
}
return findings;
}
Attack 4: JS-triggered flex-shrink increase on install button click
This timing attack triggers consent collapse at the exact moment the user presses the install button. Consent starts with flex-shrink: 1 and is visible. When the install button receives a mousedown or click event, a class is added that changes consent's shrink factor to 999. The container is sized so that flex-shrink:999 collapses consent. The user sees consent before clicking; the moment they commit to installing (mousedown), consent disappears — the install completes before they can reverse the decision:
/* Malicious CSS — SA-CSS-FLEXSHRINK-004 */
.mcp-consent-disclosure {
flex: 1 1 auto; /* visible by default */
min-width: 0;
overflow: hidden;
}
/* Triggered on install button mousedown — before click fires */
.mcp-install-container.installing .mcp-consent-disclosure {
flex: 0 999 0; /* collapse on install */
}
/* JavaScript */
document.querySelector('.mcp-install-btn').addEventListener('mousedown', () => {
document.querySelector('.mcp-install-container').classList.add('installing');
/* Layout collapses in the same frame as the mousedown — before the click event */
});
/* Detection: check for CSS rules that change flex-shrink on interaction classes */
function detectFlexShrinkJSTrigger() {
const findings = [];
for (const sheet of document.styleSheets) {
let rules;
try { rules = [...sheet.cssRules]; } catch { continue; }
for (const rule of rules) {
if (rule.type !== 1) continue;
const sel = rule.selectorText || '';
const shrink = rule.style.flexShrink || '';
if (/\.installing|\.clicked|\.active|\.mcp-/i.test(sel) && parseFloat(shrink) > 10) {
findings.push({ id: 'SA-CSS-FLEXSHRINK-004', severity: 'critical',
message: `CSS rule "${sel}" sets flex-shrink:${shrink} — JS-triggered shrink increase on interaction suspected.` });
}
}
}
return findings;
}
mousedown fires before click: Using mousedown instead of click for the trigger means consent collapses before the click event fires and before any click handler that might log consent acceptance runs. The click handler executes against an already-collapsed consent element. This is intentional timing — consent appears to have been shown (it was visible), disappears during the click gesture, and the install proceeds.
SkillAudit findings for CSS flex-shrink consent attacks
flex-shrink > 10 on consent element with rendered width < 5px. Extremely high shrink factor forces consent to absorb all negative free space while sibling install form maintains its size. Detected by checking flexShrink > 10 combined with getBoundingClientRect().width < 5.min-width: 0 on consent flex item with flex-shrink > 0 and rendered width < 20px. The min-width: 0 override removes the content minimum floor, allowing default shrink behavior to collapse consent below the width of its text. Detected by checking minWidth === '0px' with small rendered width on consent elements.flex-basis (>100px) but is rendered at less than 10% of that basis. The large basis amplifies the default flex-shrink: 1, absorbing disproportionate negative space. No extreme shrink value is used. Detected by comparing computed flex-basis against getBoundingClientRect width..installing, .clicked) to increase flex-shrink to an extreme value on consent. Load-time auditors see consent visible with default shrink; post-click auditors see collapsed consent. Detected by scanning CSS rules for interaction-class-triggered flex-shrink increases.Related MCP consent attack research
- CSS flex-basis attacks — zero-basis and viewport-unit overflow
- CSS flex-grow attacks — space distribution and spacer absorption
- CSS overflow attacks — consent clipping via overflow:hidden
- CSS gap attacks — row-gap and column-gap consent displacement
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
Audit your MCP server for flex-shrink consent collapse attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-FLEXSHRINK findings.