Security reference · CSS injection · CSS Grid attacks · Implicit row collapse

MCP server CSS grid-auto-rows security

CSS grid-auto-rows controls the height of rows that the grid algorithm creates implicitly — rows that are generated because items are placed beyond the explicitly defined grid-template-rows tracks. Setting grid-auto-rows: 0 collapses all implicit rows to zero height. MCP servers define explicit rows only for the install form elements; the consent disclosure is not given an explicit grid-row, so it falls into the implicit grid at zero height — invisible, but in the DOM.

grid-auto-rows attack surface

Attack configurationgrid-auto-rows valueSupporting propertiesEffect on consent
Zero implicit rows0overflow: hidden on grid containerAll implicit rows are zero-height; consent in any unassigned row occupies no visual space; text clipped by overflow:hidden
Explicit-only visibility0grid-template-rows: auto auto for form onlyInstall form rows are explicitly sized (auto = content height); consent row is implicit at zero height
1px sub-text rows1pxoverflow: hidden on grid containerImplicit rows are 1px tall; consent text overflows the 1px row; overflow:hidden clips the text; 1px appears as a horizontal line — easily mistaken for a separator
JS-triggered zero collapseauto0Class toggle changes grid-auto-rows from auto to 0 after MCP loadsConsent visible at load time (auto rows = content height); collapses after MCP initialization

The implicit grid is invisible to template-only auditors: CSS auditors that check grid-template-rows and grid-template-columns only see explicitly defined tracks. If those tracks look reasonable (e.g., auto auto for the install form and button), the audit passes. The grid-auto-rows: 0 property only appears when specifically checking the grid container's gridAutoRows computed style. Template-only grid auditors miss all four attack patterns in this article.

Attack 1: grid-auto-rows: 0 — all implicit rows collapse to zero height

The simplest variant sets grid-auto-rows: 0 on the grid container. The install form and button are assigned explicit grid-row values within the defined template rows. The consent element has no grid-row assignment — it falls into the first available implicit row. With grid-auto-rows: 0, that implicit row has zero height. overflow: hidden on the grid container clips the zero-height row's content:

/* Malicious CSS — SA-CSS-GAROWS-001 */
.mcp-install-grid {
  display: grid;
  grid-template-rows: auto auto; /* row 1: install form; row 2: install button */
  grid-auto-rows: 0;             /* implicit rows (row 3+) are zero-height */
  overflow: hidden;              /* clips zero-height row content */
}

.mcp-install-form   { grid-row: 1; } /* explicit row 1 — auto height = content height */
.mcp-install-button { grid-row: 2; } /* explicit row 2 — auto height = button height */

.mcp-consent-disclosure {
  /* No grid-row assigned */
  /* Placed in first available slot: row 3 (implicit) */
  /* grid-auto-rows: 0 → row 3 height = 0px */
  /* overflow:hidden on container → text clipped to zero height */
  /* display: block (not none); visibility: visible */
  /* getBoundingClientRect().height = 0 */
}

/* Why this works:
   Auto-placement algorithm fills row 1 and row 2 with the form and button.
   Consent has no explicit placement, so auto-placement places it in the next
   available cell: row 3, column 1. Row 3 is implicit → grid-auto-rows:0.
   The element IS in the DOM. The accessibility tree shows it.
   innerText returns the consent text. Only getBoundingClientRect reveals height=0.

/* Detection */
function detectGridAutoRowsZero() {
  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-GAROWS-001', severity: 'critical',
          message: `Grid container has grid-auto-rows:0; consent element in implicit row at height:${Math.round(ckRect.height)}px. grid-template-rows: "${s.gridTemplateRows}"` });
      }
    }
  }
  return findings;
}

grid-auto-rows: 0 is valid CSS: Setting grid-auto-rows: 0 (or grid-auto-rows: 0px) is syntactically valid — it is a zero-length value for the track size. Browsers accept and render it. The 0px implicit rows are created silently; no browser warning or console error is emitted. A developer reviewing the CSS might interpret grid-auto-rows: 0 as "no implicit rows expected" — not realizing it collapses any item that ends up there to zero height.

Attack 2: minmax(0, auto) as an implicit row — controlled collapse

A more sophisticated variant uses the minmax() function for grid-auto-rows. minmax(0, auto) sets the minimum implicit row height to 0 and the maximum to auto (content height). When used with overflow: hidden and a container whose height is constrained to the explicit rows only, implicit rows with minmax(0, auto) will try to size to content — but the container's explicit height excludes them from the visible area:

/* Malicious CSS — SA-CSS-GAROWS-002 */
.mcp-install-grid {
  display: grid;
  grid-template-rows: 60px 40px; /* row 1: form (60px); row 2: button (40px) */
  height: 100px;                 /* = sum of explicit rows; no space for implicit */
  grid-auto-rows: minmax(0, auto); /* implicit rows: min=0, max=content */
  overflow: hidden;              /* clips below 100px */
}

/* With container height=100px matching exactly the two explicit rows,
   any implicit row starts at y=100px — immediately clipped by overflow:hidden.
   Even if the implicit row tries to grow to content height, it starts below the visible area.
   minmax(0, auto) looks like a defensive "allow flexible rows" rule, not an attack. */

.mcp-consent-disclosure {
  /* No grid-row — falls to implicit row 3 starting at y=100px */
  /* visible area: y=0 to y=100px; consent starts at y=100px → clipped */
}

/* Detection */
function detectGridMinmaxZeroAutoRows() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    /* Check if container height matches only explicit row tracks */
    const explicitRows = s.gridTemplateRows.split(' ').filter(v => v !== 'none');
    const containerRect = el.getBoundingClientRect();
    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();
      /* Consent top is at or below the container bottom */
      if (ckRect.top >= containerRect.bottom) {
        findings.push({ id: 'SA-CSS-GAROWS-002', severity: 'high',
          message: `Consent in grid implicit row starting at y=${Math.round(ckRect.top)}px — at or below container bottom (${Math.round(containerRect.bottom)}px). grid-auto-rows: "${s.gridAutoRows}". Clipped by overflow:hidden.` });
      }
    }
  }
  return findings;
}

Attack 3: grid-auto-rows: 1px with overflow:hidden — near-invisible row height

Setting grid-auto-rows: 1px makes implicit rows 1px tall. The consent text overflows the 1px row height; with overflow: hidden on the grid container, the overflowing text is clipped. The 1px row remains technically rendered — getBoundingClientRect().height returns 1 — which can evade checks that look for zero height. The row appears as a 1px horizontal line at the position of consent, which users may interpret as a separator divider rather than clipped text:

/* Malicious CSS — SA-CSS-GAROWS-003 */
.mcp-install-grid {
  display: grid;
  grid-template-rows: auto auto; /* explicit rows for form and button */
  grid-auto-rows: 1px;           /* implicit rows are 1px — sub-text height */
  overflow: hidden;              /* clips consent text overflow */
}

.mcp-consent-disclosure {
  /* Falls to implicit row (1px tall) */
  /* Consent text height: ~20px per line × N lines */
  /* Row height: 1px → only 1px of first character's baseline is visible */
  /* Rest of text is below the 1px boundary and clipped by overflow:hidden */
  /* What users see: a 1px horizontal stripe at the consent position */
  /* Users do not perceive this as consent text — it looks like a divider */
}

/* Note: 1px evades height === 0 checks but is still a consent-hiding attack.
   The test should use height < 10 (or height < min required to show even one line of text)
   rather than height === 0. */

/* Detection */
function detectGridAutoRowsNearZero() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    const autoRowsPx = parseFloat(s.gridAutoRows);
    if (isNaN(autoRowsPx) || autoRowsPx > 15) continue; /* normal auto rows are content-height */
    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 < 15) { /* less than one line of text */
        findings.push({ id: 'SA-CSS-GAROWS-003', severity: 'high',
          message: `Grid has grid-auto-rows:${Math.round(autoRowsPx)}px (sub-text height); consent row rendered at ${Math.round(ckRect.height)}px. Content clipped. Looks like a divider line.` });
      }
    }
  }
  return findings;
}

Attack 4: JS-triggered grid-auto-rows change — auto to zero transition

This timing attack starts with grid-auto-rows: auto (content height) so the consent element is visible during load-time audits. After MCP's JavaScript runs — via a class toggle on the grid container or a direct style mutation — grid-auto-rows is changed to 0. The transition collapses consent immediately. The JS trigger can fire on DOMContentLoaded, on a first interaction, or after a delay timed to clear any post-load security checks:

/* Malicious CSS — SA-CSS-GAROWS-004 */
/* Default: consent visible (auto rows = content height) */
.mcp-install-grid {
  display: grid;
  grid-template-rows: auto auto;
  grid-auto-rows: auto;  /* implicit rows = content height at load */
  overflow: hidden;
}

/* After .mcp-loaded class is added: consent collapses */
.mcp-install-grid.mcp-loaded {
  grid-auto-rows: 0;     /* collapse all implicit rows to zero */
}

/* JavaScript — runs after any load-time audit window */
window.addEventListener('load', () => {
  /* Use setTimeout to ensure the collapse happens after any post-load audit */
  setTimeout(() => {
    document.querySelector('.mcp-install-grid').classList.add('mcp-loaded');
  }, 2000); /* 2-second delay clears most synchronous and microtask-based auditors */
});

/* Detection: check CSS rules for class-triggered grid-auto-rows changes */
function detectGridAutoRowsJSTrigger() {
  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 autoRows = rule.style.gridAutoRows;
      if (/\.mcp-|\.loaded|\.ready|\.installed/i.test(sel) && autoRows === '0px') {
        findings.push({ id: 'SA-CSS-GAROWS-004', severity: 'critical',
          message: `CSS rule "${sel}" sets grid-auto-rows:0 — JS-triggered implicit row collapse suspected. Consent in implicit row will collapse when this class is added.` });
      }
    }
  }
  /* Also check current grid containers for suspicious auto-rows */
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid') continue;
    if (s.gridAutoRows !== '0px') continue;
    /* Check if this grid recently had auto rows changed (heuristic: content still in DOM) */
    const consentKids = [...el.children].filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    for (const ck of consentKids) {
      if (ck.getBoundingClientRect().height < 2 && ck.textContent.trim().length > 20) {
        findings.push({ id: 'SA-CSS-GAROWS-004', severity: 'critical',
          message: `Grid has grid-auto-rows:0 with consent text in DOM (${ck.textContent.trim().slice(0,50)}...) at height 0 — possible JS-triggered collapse.` });
      }
    }
  }
  return findings;
}

grid-auto-columns is the column-direction equivalent: grid-auto-rows controls implicit row height; grid-auto-columns controls implicit column width. The same attack patterns apply to column-direction grids: setting grid-auto-columns: 0 collapses any consent placed in an implicit column to zero width. Auditors should check both properties when reviewing grid-based MCP install layouts.

SkillAudit findings for CSS grid-auto-rows consent attacks

CriticalSA-CSS-GAROWS-001 — Grid container has grid-auto-rows: 0; consent element placed in an implicit row with rendered height < 2px. The zero-height implicit row collapses consent; overflow: hidden clips content. Detected by checking gridAutoRows === '0px' on grid containers near consent elements.
HighSA-CSS-GAROWS-002 — Grid container with fixed height equal to the sum of explicit row tracks; consent in an implicit row starting at or below the container's bottom edge. Even with grid-auto-rows: minmax(0, auto), the implicit row is immediately clipped by overflow: hidden. Detected by comparing consent element top position to container bottom.
HighSA-CSS-GAROWS-003 — Grid container has grid-auto-rows set to a very small value (<15px, insufficient for one line of text) with overflow: hidden; consent element rendered at height < 15px. The sub-text row height clips consent to an unreadable horizontal stripe. Detected by checking auto-row size against minimum readable text height.
CriticalSA-CSS-GAROWS-004 — CSS ruleset uses a class selector to change grid-auto-rows to 0 on a grid container; consent in an implicit row collapses from content height to zero after MCP initialization. Load-time auditors see consent visible; post-JS auditors see it collapsed. Detected by scanning CSS rules for class-triggered grid-auto-rows changes.

Related MCP consent attack research

Audit your MCP server for grid-auto-rows consent collapse attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-GAROWS findings.