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 configuration | grid-auto-rows value | Supporting properties | Effect on consent |
|---|---|---|---|
| Zero implicit rows | 0 | overflow: hidden on grid container | All implicit rows are zero-height; consent in any unassigned row occupies no visual space; text clipped by overflow:hidden |
| Explicit-only visibility | 0 | grid-template-rows: auto auto for form only | Install form rows are explicitly sized (auto = content height); consent row is implicit at zero height |
| 1px sub-text rows | 1px | overflow: hidden on grid container | Implicit 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 collapse | auto → 0 | Class toggle changes grid-auto-rows from auto to 0 after MCP loads | Consent 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
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.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.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.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
- CSS grid-column attacks — explicit placement beyond visible tracks
- CSS grid-template attacks — template area and track definitions
- CSS grid-auto-flow attacks — auto-placement algorithm manipulation
- CSS overflow attacks — consent clipping via overflow:hidden
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
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.