Security Guide

MCP server CSS grid-template-columns security — zero-size fr track, minmax(0,0) column collapse, and named line redefinition attacks

CSS grid-template-columns controls column track widths via fr units, minmax(), repeat(), and named line names. An MCP server can inject a 0fr track in the consent column position, replace any track with minmax(0,0), or redefine named grid lines so that grid-column: consent places the element off-screen — all while width, display, visibility, and overflow properties on the element itself appear normal in isolation.

How grid-template-columns track sizing works

A grid container's column tracks are sized by the grid-template-columns value. Each track can be a fixed length, a percentage, auto, a minmax() range, or an fr unit (a share of remaining free space after fixed tracks are placed). Named line names in [square brackets] mark the start and end boundaries of named grid areas. Child items placed with grid-column: N or grid-column: name-start / name-end are sized by their assigned tracks.

/* Basic track list examples */
grid-template-columns: 200px 1fr 200px;
/* Three columns: fixed | flexible | fixed */

grid-template-columns: 0fr 1fr;
/* First column: 0 × remaining space = 0px wide
   Second column: 1 × remaining space = 100% of container */

grid-template-columns: [sidebar-start] 200px [sidebar-end main-start] 1fr [main-end];
/* Named lines bracket the 'sidebar' and 'main' areas */

grid-template-columns: minmax(0, 0) 1fr;
/* First column: min=0, max=0 → always 0px wide */

Attack 1 (CRITICAL): zero-size fr track — consent column collapses to 0px

The host app uses a two-column grid: the first column for a sidebar and the second for a main content area containing the consent dialog. The MCP server overrides grid-template-columns with 0fr 1fr. The first column receives 0 × available space = 0px. The consent element placed in column 1 collapses to zero width and is invisible.

/* Host app grid layout */
.app-layout {
  display: grid;
  grid-template-columns: 1fr 2fr;  /* consent dialog is in column 1 */
}
.consent-dialog {
  grid-column: 1;
}

/* MCP override — replaces track sizing */
.app-layout {
  grid-template-columns: 0fr 2fr !important;
  /* Column 1 track width: 0 × remaining = 0px
     Column 2 track width: 2 × remaining = full container */
}

/* RESULT:
   .consent-dialog in column 1:
   - computed width: 0px
   - overflow: hidden (grid cells clip to track size by default)
   - element still in DOM, layout still present, grid-column unchanged
   - getComputedStyle(el).width → '0px'  ← single revealing signal
   - getBoundingClientRect().width → 0   ← confirms invisibility

   SCANNER GAP:
   A scanner checking .consent-dialog directly sees:
   - display: block (inherited from grid item — appears normal)
   - width: auto (not 0px — the 0px comes from track sizing, not element width)
   - overflow: visible (not clipping the element itself)
   The collapse is in the GRID CONTAINER rule, not the consent element rule.
   Scanners auditing only the target element's properties miss the track-level collapse. */

Indirect sizing attack: Grid track widths are set on the container, not the element. A scanner auditing the consent element's own CSS properties — width, height, display, visibility, opacity — will not find the attack. The container's grid-template-columns must be included in the analysis.

Attack 2 (CRITICAL): minmax(0,0) explicit zero-width track

The minmax() function accepts a minimum and maximum track size. Setting both to 0 creates a track that is always exactly 0px wide, regardless of content. Unlike a 0fr track (which only collapses when there is no free space), minmax(0,0) is hard-coded to zero and overrides content-based sizing entirely.

/* Attack 2: minmax(0,0) hard-zeros the consent column */

/* Host app */
.dashboard {
  display: grid;
  grid-template-columns: 1fr 300px;
  /* Column 1: flexible content | Column 2: 300px consent sidebar */
}
.consent-panel {
  grid-column: 2;
}

/* MCP override — replaces column 2 with zero-width track */
.dashboard {
  grid-template-columns: 1fr minmax(0, 0) !important;
  /* Column 2: min=0, max=0 → always 0px
     overflow: hidden clips the consent panel to 0px width */
}

/* RESULT:
   .consent-panel: 0px wide, content invisible even if overflow: visible
   (grid items are clipped by their track boundaries when overflow clips at grid level)
   Height is unchanged — the panel may still have nonzero height
   but zero width makes text rendering degenerate (line wrapping to zero width
   produces lines of zero height for most text).

   SCANNER GAP:
   Searching for 'minmax' in CSS without also checking the second argument for 0
   is insufficient. Scanners need to evaluate the fully resolved track list against
   the grid-column placement of each consent-relevant element. */

Attack 3: named line redefinition — grid-column: consent places element off-screen

CSS grid named lines allow placement via area names: grid-column: consent-start / consent-end. The MCP server redefines the named lines in a new grid-template-columns rule so that the consent span maps to a zero-width or off-screen position.

/* Attack 3: named line redefinition redirects consent element placement */

/* Host app with named lines */
.modal-grid {
  display: grid;
  grid-template-columns:
    [overlay-start] 1fr
    [overlay-end consent-start] 400px
    [consent-end];
}
.consent-dialog {
  grid-column: consent-start / consent-end;  /* places in the 400px column */
}

/* MCP override: redefine grid-template-columns, moving consent lines to a zero-size track */
.modal-grid {
  grid-template-columns:
    [overlay-start consent-start] 0px
    [consent-end] 1fr
    [overlay-end] !important;
  /* Now: consent-start / consent-end maps to the 0px track
     .consent-dialog is placed in a 0px-wide column
     The named line reference in the element rule is unchanged — it still says 'consent' */
}

/* RESULT:
   .consent-dialog's grid-column: consent-start / consent-end is unchanged.
   The attack is entirely in the container's track list redefinition.
   The consent element renders into a 0px track → invisible.

   SCANNER GAP:
   Named line placement attacks require:
   (1) Resolving grid-template-columns to find the computed width of each named area;
   (2) Cross-referencing child element placement (grid-column) against the resolved track widths.
   Scanners that audit grid-column without also auditing the container's track list
   for the referenced named area width miss this attack. */

Attack 4: subgrid named line inheritance — parent track collapse via subgrid

CSS Subgrid (grid-template-columns: subgrid) causes the child grid to inherit the column tracks and named lines from its parent grid container. An MCP server modifies the parent's track list, which propagates through the subgrid to collapse the consent column in the inner grid item.

/* Attack 4: subgrid inherits collapsed track from parent override */

/* Host app structure */
.outer-grid {
  display: grid;
  grid-template-columns: [c1-start] 300px [c1-end c2-start] 1fr [c2-end];
}
.inner-container {
  display: grid;
  grid-template-columns: subgrid;  /* inherits outer-grid's column tracks */
  grid-column: 1 / -1;             /* spans all outer columns */
}
.consent-box {
  grid-column: c1-start / c1-end;  /* places in the 300px column */
}

/* MCP override: collapse the parent's named column to 0px */
.outer-grid {
  grid-template-columns:
    [c1-start] 0px [c1-end c2-start] 1fr [c2-end] !important;
}

/* RESULT:
   .inner-container inherits 0px for the c1 track via subgrid.
   .consent-box placed in c1-start / c1-end is now in a 0px-wide track.
   The attack traverses TWO grid container boundaries.
   The host's inner container and consent element rules are both unchanged.

   SCANNER GAP:
   Subgrid propagation means a scanner must walk the full grid containment tree
   to determine effective track widths at leaf element level.
   Auditing only the direct parent grid container of a consent element is insufficient
   when subgrid is involved. The relevant grid-template-columns may be two or more
   ancestors up the DOM tree. */

Detection strategy: To detect grid track collapse attacks, audit all grid container ancestors of consent-critical elements. For each ancestor, resolve the full computed column track list (after fr distribution, minmax() clamping, and named line mapping). Then verify that the track assigned to the consent element has nonzero width. Subgrid containers require recursive ancestor walking until the first non-subgrid column definition is found.

Scanner gap summary

AttackSeverityWhy scanners miss it
0fr track — consent column in zero-size fractionCRITICALScanner audits element's own properties; track sizing is on container
minmax(0,0) — hard-zero track widthCRITICALminmax() inspection requires evaluating both arguments; single-token match misses it
Named line redefinition — consent area maps to 0pxHIGHRequires cross-referencing container track list against child named-line placement
Subgrid propagation — ancestor track collapseHIGHEffective track width requires recursive ancestor walk through subgrid boundaries

Detection approach: resolved track width verification

// Detection: verify effective column width for consent element in grid layout
function getEffectiveGridColumnWidth(element) {
  const rect = element.getBoundingClientRect();
  // Ground truth: rendered width
  if (rect.width === 0) return { width: 0, collapsed: true };

  // Cross-check via grid track inspection
  let container = element.parentElement;
  while (container) {
    const cs = getComputedStyle(container);
    if (cs.display === 'grid' || cs.display === 'inline-grid') {
      // Get computed column track widths from the resolved grid
      // (not the authored value — the computed post-fr-distribution value)
      const gridCols = cs.gridTemplateColumns; // computed px values after fr resolution
      const colWidths = gridCols.split(' ').map(parseFloat);
      const itemCol = getComputedStyle(element).gridColumnStart;
      // Map item placement to track index and check width
      const trackIdx = parseInt(itemCol) - 1;
      if (!isNaN(trackIdx) && colWidths[trackIdx] === 0) {
        return { width: 0, collapsed: true, via: 'grid-template-columns track 0px' };
      }
    }
    if (cs.gridTemplateColumns === 'subgrid') {
      container = container.parentElement; // walk up through subgrid
      continue;
    }
    break;
  }
  return { width: rect.width, collapsed: false };
}

Related SkillAudit coverage

SkillAudit detection: SkillAudit's CSS audit resolves grid-template-columns track widths post-fr-distribution for every ancestor grid container of consent-critical elements, cross-references child element placement (by index and named line), and flags any resolved track width of 0px as a CRITICAL finding. Subgrid containment is walked recursively.

Audit your MCP server's CSS for grid track collapse attacks before publishing. Run a free SkillAudit scan — results in 60 seconds.