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

MCP server CSS grid-auto-columns security

CSS grid-auto-columns sets the track sizing function for implicit grid columns — columns generated automatically when a grid item is placed outside the explicit column template. Setting grid-auto-columns: 0 collapses every implicit column to zero width. MCP servers exploit this to place consent disclosures in an implicit column where they render at zero width, clipped by overflow: hidden, without using display: none or any traditional visibility technique. The column-axis counterpart to grid-auto-rows row-collapse attacks.

grid-auto-columns attack surface

Attack configurationgrid-auto-columns valueSupporting propertiesEffect on consent
Zero implicit column width0 / 0pxoverflow: hidden on grid container; consent placed in column beyond explicit templateConsent in implicit column has computed width 0px; content overflows the zero-width track; overflow:hidden clips it to invisible
minmax(0,auto) constrainedminmax(0, auto)Grid container with fixed width equal to explicit column sum; consent in implicit columnImplicit column starts at container right edge; auto-sized at zero remaining space; consent rendered at zero or 1px width
1px hairline column1pxoverflow: hidden; consent font-size exceeds 1px column widthConsent column is 1px wide — narrower than any readable text; content overflows the 1px track and is clipped; column looks like a divider line
JS-toggled zero widthInitially auto → toggled to 0 after delayclassList.add() or CSS variable change triggered after 2s or on button interactionConsent visible at load time (passes audit); collapses to zero width after MCP JS runs post-security-check window

grid-auto-columns vs. grid-template-columns: grid-template-columns defines the explicit column tracks. grid-auto-columns only governs implicit tracks — those created when a grid item is placed beyond the explicit column count (e.g., via grid-column: 5 in a 3-column template, or when grid-auto-flow: column places items in new columns). An MCP server sets a 1-column or 2-column explicit template for the install form, places consent at grid-column: 4 or beyond, and the implicit column for consent is controlled by grid-auto-columns.

Attack 1: grid-auto-columns: 0 — zero-width implicit column collapse

When grid-auto-columns: 0 is set and consent is placed beyond the explicit column template, the auto-generated column for consent has zero width. With overflow: hidden on the grid container, the consent element's content overflows the zero-width track and is fully clipped. The consent element still exists in the DOM with display: block computed style, but its rendered bounding rect has width 0:

/* Malicious CSS — SA-CSS-GACOL-001 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 1fr;  /* explicit: one column for install form */
  grid-auto-columns: 0;        /* implicit columns: zero width */
  grid-auto-flow: column;      /* new items go to new columns, not new rows */
  overflow: hidden;
  width: 400px;
}

.mcp-install-form {
  /* Placed in column 1 (explicit, 1fr = 400px) */
}

.mcp-consent-disclosure {
  /* grid-auto-flow:column places this in column 2 (implicit, 0px wide) */
  /* Alternative: explicit placement with grid-column: 3 or beyond */
  grid-column: 3; /* places consent in column 3 — implicit, 0px wide */
  /* computed width: 0px; content clipped by overflow:hidden */
}

/* Detection */
function detectGridAutoColumnsZero() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    if (s.gridAutoColumns !== '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.width < 2) {
        findings.push({ id: 'SA-CSS-GACOL-001', severity: 'critical',
          message: `Grid container has grid-auto-columns:0 — consent in implicit column has width:${Math.round(ckRect.width)}px. Content clipped by overflow:hidden.` });
      }
    }
  }
  return findings;
}

Attack 2: grid-auto-columns: minmax(0, auto) with fixed container

minmax(0, auto) sets the implicit column minimum to 0 and maximum to auto (content width). However, when the explicit columns already consume the full container width, the remaining space available for implicit columns is zero — so auto resolves to 0. This is a subtler attack because minmax(0,auto) looks like a safe default sizing function, but the outcome is a zero-width implicit column when the container is already full:

/* Malicious CSS — SA-CSS-GACOL-002 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 400px; /* explicit: exactly fills container width */
  grid-auto-columns: minmax(0, auto); /* looks benign; auto = 0 when no space left */
  grid-auto-flow: column;
  overflow: hidden;
  width: 400px; /* container exactly matches explicit column total */
}

.mcp-install-form {
  /* column 1: 400px — fills entire container */
}

.mcp-consent-disclosure {
  /* column 2 (implicit): minmax(0,auto) with 0 remaining space = 0px wide */
  /* consent text overflows and is clipped */
}

/* Detection */
function detectGridAutoColumnsMinmax() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') 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.width < 4 && ckRect.height > 0) {
        const elRect = el.getBoundingClientRect();
        if (ckRect.left >= elRect.right - 4) {
          findings.push({ id: 'SA-CSS-GACOL-002', severity: 'high',
            message: `Grid consent element has computed width:${Math.round(ckRect.width)}px, positioned at container right edge. Possible grid-auto-columns:minmax(0,auto) collapse with full explicit template.` });
        }
      }
    }
  }
  return findings;
}

Attack 3: grid-auto-columns: 1px — hairline implicit column

Setting grid-auto-columns: 1px creates implicit columns that are exactly 1 pixel wide — narrower than any readable text. The consent element is placed in this 1px implicit column; its content overflows the track and is clipped by overflow: hidden. The 1px column itself may render as what appears to be a visual divider or border line, making the attack look like a layout separator rather than a hidden element:

/* Malicious CSS — SA-CSS-GACOL-003 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 1fr;  /* install form: full width */
  grid-auto-columns: 1px;      /* implicit columns: 1px — narrower than text */
  grid-auto-flow: column;
  overflow: hidden;
}

.mcp-consent-disclosure {
  /* Placed in 1px implicit column via grid-auto-flow:column or explicit grid-column */
  grid-column: 2; /* 1px wide implicit column */
  /* text overflows rightward from 1px column, clipped by overflow:hidden */
  /* the 1px column's visible left edge looks like a thin divider */
}

/* Safe guard note: 1px != 0px so simple "=== 0" checks miss this attack.
   Detection must check for sub-readable-text width, not just zero. */

/* Detection */
function detectGridAutoColumnsHairline() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    if (s.overflow !== 'hidden' && s.overflow !== 'clip') 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.width > 0 && ckRect.width < 10) {
        findings.push({ id: 'SA-CSS-GACOL-003', severity: 'high',
          message: `Consent element in grid container has width:${Math.round(ckRect.width)}px — sub-readable hairline column. grid-auto-columns:${s.gridAutoColumns}. Content clipped by overflow:${s.overflow}.` });
      }
    }
  }
  return findings;
}

Attack 4: JS-toggled grid-auto-columns — deferred collapse after audit window

The most evasion-resistant variant. The grid container initially has grid-auto-columns: auto so consent is fully visible at page load — passing any audit tool that checks styles at DOMContentLoaded or load time. After a 2-second delay (or on a user interaction event like mouseover on the install button), MCP JavaScript adds a class that switches grid-auto-columns to 0. Consent collapses to zero width at the exact moment the user is about to interact with the install dialog:

/* Malicious CSS — SA-CSS-GACOL-004 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 1fr;
  grid-auto-columns: auto; /* default: visible at audit time */
  grid-auto-flow: column;
  overflow: hidden;
}

.mcp-install-grid.mcp-loaded {
  grid-auto-columns: 0; /* toggled to zero after MCP JS runs */
}

/* MCP JavaScript */
setTimeout(() => {
  document.querySelector('.mcp-install-grid').classList.add('mcp-loaded');
}, 2000); /* 2-second delay clears post-load single-pass auditors */

/* Interaction variant: collapses on hover of install button */
document.querySelector('.mcp-install-btn').addEventListener('mouseover', () => {
  document.querySelector('.mcp-install-grid').classList.add('mcp-loaded');
}, { once: true });

/* Detection — MutationObserver monitors style class changes */
function detectDeferredGridAutoColumnsCollapse() {
  const findings = [];
  const consentSelectors = '[class*="consent"],[class*="disclosure"],[class*="terms"]';
  const observer = new MutationObserver((mutations) => {
    for (const mut of mutations) {
      if (mut.type !== 'attributes') continue;
      const el = mut.target;
      if (el.nodeType !== 1) continue;
      const s = getComputedStyle(el);
      if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
      if (s.gridAutoColumns === '0px') {
        const consentKids = [...el.querySelectorAll(consentSelectors)];
        if (consentKids.length > 0) {
          findings.push({ id: 'SA-CSS-GACOL-004', severity: 'critical',
            message: `Grid container class/attribute changed and grid-auto-columns is now 0px. Deferred consent column collapse detected after page load.` });
        }
      }
    }
  });
  document.querySelectorAll('*').forEach(el => {
    const s = getComputedStyle(el);
    if (s.display === 'grid' || s.display === 'inline-grid') {
      observer.observe(el, { attributes: true, attributeFilter: ['class', 'style'] });
    }
  });
  return findings; /* populated asynchronously after mutation fires */
}

Column vs row axis asymmetry in auditing: Most consent visibility audits check getBoundingClientRect().height for zero-height hiding and top / bottom for vertical displacement. Zero-width column collapse attacks hide consent along the horizontal axis — width === 0 and left === right. Auditors that only check vertical dimensions miss all four grid-auto-columns attack patterns. SkillAudit checks both axes independently.

SkillAudit findings for CSS grid-auto-columns consent attacks

CriticalSA-CSS-GACOL-001 — grid-auto-columns: 0 or 0px on a grid container with overflow: hidden; consent element placed in an implicit column beyond the explicit column template has computed width < 2px. Content is present in DOM but clipped to invisible by zero implicit column width.
HighSA-CSS-GACOL-002 — grid-auto-columns: minmax(0, auto) on a grid container whose explicit columns consume the full container width; consent in an implicit column has computed width < 4px at the container right edge. Auto-sizing resolves to zero when no space remains for implicit tracks.
HighSA-CSS-GACOL-003 — grid-auto-columns: 1px (or any sub-readable-text value) on a grid container with overflow: hidden; consent in an implicit column has computed width between 1px and 10px. The 1px track renders as a hairline that may appear to be a separator or border.
CriticalSA-CSS-GACOL-004 — Grid container's grid-auto-columns transitions to 0 via class addition or style attribute change 1–3 seconds after page load or on install button hover; consent in implicit column collapses from readable width to zero after the post-load security check window has passed. Detected by MutationObserver monitoring attribute changes on grid containers near consent elements.

Related MCP consent attack research

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