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 configurationflex-grow (consent)Supporting propertiesEffect on consent
Zero-grow lock0flex-basis: 0; min-width: 0; overflow: hiddenConsent stays at flex-basis (zero); all positive free space goes to install form with flex-grow:1; consent rendered at 0px width
Spacer absorption1Spacer element with flex-grow: 999 inserted between form and consentSpacer takes 999/1000 of free space; consent claims 1/1000 — renders as 1-2px
JS-toggled collapse1 initially → 0Class toggle on .mcp-loaded changes grow from 1 to 0Consent visible at load time (passes audit); hidden after MCP script runs
Pre-consumed space1An invisible flex-grow: 1 sibling with height: 0; overflow: hidden — zero visible but full grow factorInvisible 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

CriticalSA-CSS-FLEXGROW-001 — 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.
HighSA-CSS-FLEXGROW-002 — Flex container contains an element with 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.
CriticalSA-CSS-FLEXGROW-003 — CSS ruleset uses a class selector (e.g., .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.
HighSA-CSS-FLEXGROW-004 — Flex container contains one or more invisible siblings (zero height/width, no consent text) with 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

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.