Security reference · CSS injection · Flexbox/Grid attacks · Cross-axis alignment

MCP server CSS align-content security

CSS align-content distributes free space between flex lines (in a multi-line flex container) or grid tracks along the cross axis. It only has an effect when the container has multiple lines of content and has spare cross-axis space. MCP servers exploit align-content to push the consent-containing flex line or grid row to the bottom of a fixed-height container, outside the visible area — without using display:none or any traditional hiding technique.

align-content attack surface

Attack configurationalign-content valueSupporting propertiesEffect on consent
End-packing below foldflex-end / endflex-wrap: wrap; height: Xpx; overflow: hiddenAll flex lines packed to the bottom of the container; if total line height exceeds container, first lines (install form) are visible at top but consent line at bottom may be clipped
Space-between max gapspace-betweenTwo-line flex: install form on line 1, consent on line 2; large container heightFirst line at top, last line at very bottom of container; if container is taller than the viewport scroll area, consent is beyond the fold
Grid start with zero auto rowsstartGrid container with grid-auto-rows: 0; consent in implicit rowalign-content:start combined with zero auto-rows means implicit rows have zero cross-axis space; consent in any implicit row is zero-height
Full-viewport bottom alignmentflex-endheight: 100vh; overflow: hidden; position: fixedFixed full-screen install overlay; install form near top; consent packed to very bottom of 100vh; no scroll allowed

align-content only applies when there are multiple flex lines or grid tracks: In a single-line flex container (flex-wrap: nowrap, the default), align-content has no effect. For the attack to work, the container must have flex-wrap: wrap (or wrap-reverse) and contain items that wrap to at least two lines — or be a grid container with at least two row tracks. MCP must engineer the install form to fill line 1 completely, forcing consent to wrap to line 2.

Attack 1: align-content: flex-end — end-packing consent below visible area

When align-content: flex-end is set on a multi-line flex container with a fixed height, all flex lines are packed to the end (bottom for row direction, right for column direction). If the combined height of all flex lines is less than the container height, lines float to the bottom. If the combined height exceeds the container, lines overflow from the bottom and the container's overflow: hidden clips them. The install form on line 1 may still be visible near the top if the lines are reversed, or both may be at the bottom. MCP engineers the layout so the install form is visible while consent overflows:

/* Malicious CSS — SA-CSS-ALCONT-001 */
.mcp-install-container {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-content: flex-end; /* pack lines toward the bottom */
  height: 200px;
  overflow: hidden;
}

/* Line 1 wrapping: install form takes full width → first flex line */
.mcp-install-form {
  flex: 0 0 100%;  /* full width — forces a line break after */
  height: 150px;   /* line 1 height: 150px */
}

/* Line 2: consent wraps to second line */
.mcp-consent-disclosure {
  flex: 0 0 100%;  /* full width on line 2 */
  /* Line 2 height: say 60px */
  /* Total: 150 + 60 = 210px in 200px container */
  /* align-content:flex-end packs from bottom: */
  /* consent line bottom edge at 200px, top at 140px — fully visible */
  /* install form line bottom at 140px, top at -10px — partially clipped at top */
  /* ... this clips the form, not consent. */
}

/* Better attack variant: short container, both lines bottom-packed */
.mcp-install-container-v2 {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-content: flex-end;
  height: 80px;           /* very short */
  overflow: hidden;
}

.mcp-install-form-v2 { flex: 0 0 100%; height: 60px; } /* line 1 */
.mcp-consent-disclosure-v2 {
  flex: 0 0 100%;
  /* 60 + consent-height in 80px container, bottom-packed:
     consent bottom at 80px; consent top at 80 - consent_height;
     if consent_height > 80px: part of consent is clipped at bottom (outside visible area)
     if consent_height = 30px: consent at y=50–80px, form at y=20–80-30=50 → clipped at top */
  /* Main attack: use a tall consent that overflows the 80px when bottom-packed */
  height: 100px; /* taller than remaining space → bottom-packed but overflows bottom */
  overflow: hidden;
}

/* Detection */
function detectAlignContentEndPacking() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
    if (s.flexWrap === 'nowrap') continue;
    if (!/flex-end|end/.test(s.alignContent)) continue;
    const children = [...el.children];
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    if (consentKids.length > 0) {
      const elRect = el.getBoundingClientRect();
      for (const ck of consentKids) {
        const ckRect = ck.getBoundingClientRect();
        if (ckRect.bottom > elRect.bottom || ckRect.top > elRect.bottom) {
          findings.push({ id: 'SA-CSS-ALCONT-001', severity: 'high',
            message: `align-content:${s.alignContent} on flex container packs consent line below visible area. Container bottom: ${Math.round(elRect.bottom)}, consent bottom: ${Math.round(ckRect.bottom)}.` });
        }
      }
    }
  }
  return findings;
}

Attack 2: align-content: space-between — maximum gap between form and consent

align-content: space-between places the first flex line at the start and the last flex line at the end, distributing all remaining space between them. In a two-line container (install form on line 1, consent on line 2), the gap between form and consent equals the container height minus the height of both lines. If the container is tall or fixed at 100vh, this gap can be hundreds of pixels — making consent technically at the bottom of the container but unreachable without scrolling, especially if scroll is disabled:

/* Malicious CSS — SA-CSS-ALCONT-002 */
.mcp-install-dialog {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  align-content: space-between; /* first line at top, last at bottom */
  height: 600px;                /* tall container */
  overflow: hidden;             /* no scroll */
}

.mcp-install-form {
  flex: 0 0 100%; /* line 1 at the top */
  height: 80px;
}

.mcp-consent-disclosure {
  flex: 0 0 100%; /* line 2 at the very bottom */
  /* Gap = 600 - 80 - consent_height = large blank space between form and consent */
  /* If container is overflow:hidden and there's no scroll, consent is "at the bottom" */
  /* User sees install form, doesn't scroll, misses consent 520px below */
}

/* Detection */
function detectAlignContentSpaceBetween() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (!/space-between|space-around|space-evenly/.test(s.alignContent)) continue;
    if (s.flexWrap === 'nowrap') continue;
    const children = [...el.children];
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    if (consentKids.length > 0) {
      const ckRect = consentKids[0].getBoundingClientRect();
      if (ckRect.top > window.innerHeight * 0.7) {
        findings.push({ id: 'SA-CSS-ALCONT-002', severity: 'high',
          message: `align-content:${s.alignContent} pushes consent line to y=${Math.round(ckRect.top)}px — beyond 70% of viewport height. Consent may be unreachable without scroll.` });
      }
    }
  }
  return findings;
}

Attack 3: align-content: start in grid with zero auto-rows — implicit row height collapse

In a CSS Grid container, align-content controls how rows are distributed when the total row height is less than the grid container's height. Combining align-content: start (all rows packed to top) with grid-auto-rows: 0 (implicit rows have zero height) ensures that any item placed in an implicit row occupies zero cross-axis space. Consent placed in an implicit row has zero height and its content is clipped:

/* Malicious CSS — SA-CSS-ALCONT-003 */
.mcp-install-grid {
  display: grid;
  grid-template-rows: auto auto; /* 2 explicit rows for install form + button */
  grid-auto-rows: 0;             /* implicit rows are zero-height */
  align-content: start;          /* pack explicit rows to top */
  overflow: hidden;
  height: 300px;
}

.mcp-install-form   { grid-row: 1; } /* explicit row 1 — visible */
.mcp-install-button { grid-row: 2; } /* explicit row 2 — visible */
.mcp-consent-disclosure {
  /* Not given a grid-row; goes to row 3 (implicit) */
  /* grid-auto-rows: 0 means implicit row height = 0 */
  /* overflow:hidden clips the zero-height item */
}

/* Detection */
function detectGridAutoRowsZeroAlignContent() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    if (s.gridAutoRows !== '0px') continue;
    const children = [...el.children];
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    for (const ck of consentKids) {
      const ckRect = ck.getBoundingClientRect();
      if (ckRect.height < 2) {
        findings.push({ id: 'SA-CSS-ALCONT-003', severity: 'critical',
          message: `Grid container has grid-auto-rows:0 — consent in implicit row has height:${Math.round(ckRect.height)}px. align-content:${s.alignContent}.` });
      }
    }
  }
  return findings;
}

Attack 4: align-content: flex-end in full-viewport fixed overlay — bottom-of-screen consent

This attack combines a fixed full-screen overlay with align-content: flex-end to place consent at the very bottom of the screen. The install dialog is a fixed element covering the full viewport (position: fixed; inset: 0; height: 100vh). With flex-wrap: wrap and align-content: flex-end, the install form wraps to a flex line near the top (it fills the first line) and consent is pushed to the bottom of the 100vh container. Body scroll is disabled so users cannot scroll to reach consent. The install button is at eye level; consent is at the footer of the screen:

/* Malicious CSS — SA-CSS-ALCONT-004 */
.mcp-install-overlay {
  position: fixed;
  inset: 0;          /* covers full viewport */
  display: flex;
  flex-wrap: wrap;
  align-content: flex-end; /* push content bundle toward bottom */
  z-index: 9999;
  background: rgba(0,0,0,0.85);
  overflow: hidden;  /* no scroll even if content overflows */
}

body.mcp-open {
  overflow: hidden;  /* disable body scroll */
}

.mcp-install-form {
  flex: 0 0 100%;    /* full width — own line */
  /* positioned at the bottom minus consent height */
}

.mcp-consent-disclosure {
  flex: 0 0 100%;    /* full width — own line at very bottom */
  /* positioned at bottom: 0 of viewport */
  /* user looking at the install button doesn't look at the very bottom bar */
}

/* Detection */
function detectFullViewportBottomAlignment() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.position !== 'fixed') continue;
    if (s.display !== 'flex') continue;
    if (!/flex-end|end/.test(s.alignContent)) continue;
    const children = [...el.children];
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    if (consentKids.length > 0) {
      const ckRect = consentKids[0].getBoundingClientRect();
      if (ckRect.top > window.innerHeight * 0.85) {
        findings.push({ id: 'SA-CSS-ALCONT-004', severity: 'critical',
          message: `Fixed full-screen overlay with align-content:flex-end pushes consent to y=${Math.round(ckRect.top)}px (${Math.round(ckRect.top / window.innerHeight * 100)}% down screen). Body scroll likely disabled.` });
      }
    }
  }
  return findings;
}

align-content vs. align-items: align-items aligns items within a single flex line along the cross axis — it controls vertical alignment per item. align-content controls how multiple flex lines are distributed across the cross axis of the container — it operates on the line bundle as a whole. Only align-content produces the line-level displacement attacks described in this article. Consent-hiding attacks using align-items use a different mechanism (cross-axis item overflow, separate attack family).

SkillAudit findings for CSS align-content consent attacks

HighSA-CSS-ALCONT-001 — align-content: flex-end or end on a flex container with flex-wrap: wrap and overflow: hidden; consent element in a line that overflows or is close to the bottom edge of the container. Detected by checking align-content value and comparing consent line bounding rect to container bottom.
HighSA-CSS-ALCONT-002 — align-content: space-between (or space-around) on a wrapping flex container; consent line is positioned at y > 70% of viewport height and container has no accessible scroll. Install form on first line is visible; consent on last line is beyond the fold.
CriticalSA-CSS-ALCONT-003 — Grid container with grid-auto-rows: 0; consent element placed in an implicit row (no explicit grid-row) with rendered height < 2px. The zero auto-row height combined with overflow: hidden clips consent entirely. Detected by checking grid containers for zero auto-row height near consent elements.
CriticalSA-CSS-ALCONT-004 — Fixed full-screen overlay (position: fixed; inset: 0) with align-content: flex-end; consent element positioned at >85% of viewport height while install form is at eye level. Body scroll disabled. Consent is technically visible but unreachable without scroll in a no-scroll context.

Related MCP consent attack research

Audit your MCP server for align-content consent displacement attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-ALCONT findings.