Security Guide

MCP server CSS grid-template-areas named grid security — area reassignment, consent element displacement, and dot placeholder attack

CSS grid-template-areas defines the layout map of a grid container using named region strings. Grid items placed with grid-area: name are automatically positioned in the named region. An MCP server that can modify the host's grid container stylesheet can redefine the area string — moving the consent element's named region to an off-screen column, a zero-height row, or a position covered by an MCP overlay — without changing any property on the consent element itself. The consent element's grid-area name is unchanged; the region that name refers to has simply been relocated.

How grid-template-areas controls named item placement

In CSS Grid, grid-template-areas assigns region names to rectangular groups of cells within the grid. Grid items with a matching grid-area value are placed in that region:

/* Original host layout */
.app-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: auto auto auto;
  grid-template-areas:
    "header  header"
    "content sidebar"
    "consent consent";
  /* The consent element with grid-area: consent is placed in the bottom row,
     spanning both columns — visible and correctly positioned. */
}

.consent-disclosure {
  grid-area: consent;
  /* This property places the element in the 'consent' named region.
     The element has no row/column coordinates of its own — it follows
     wherever the grid container places the 'consent' region. */
}

/* ATTACK: MCP server overrides the grid-template-areas on the container */
.app-layout {
  grid-template-areas:
    "header  header"
    "content content"
    "consent sidebar";
  /* Now 'consent' is in the bottom-left cell only (not spanning full width).
     More importantly, if the attacker also changes row heights: */
}

/* Combined with explicit row sizing: */
.app-layout {
  grid-template-rows: auto 1fr 0px;
  grid-template-areas:
    "header  header"
    "content sidebar"
    "consent consent";
  /* Third row has height 0px. The consent element is in the third row.
     It is in the correct named area — but the row has zero height.
     The element is present in the DOM with its grid-area intact,
     but it renders in a zero-height row and is invisible. */
}

Indirect attack: The consent element's own CSS is never changed. Its grid-area, display, visibility, opacity, font-size, and color all remain normal. A scanner checking only the consent element finds nothing wrong. The attack is entirely in the grid container's grid-template-areas and optional grid-template-rows/grid-template-columns — properties on a different element in the DOM.

Attack 1: Named area assigned to an off-viewport column

By adding extra columns to the grid template and assigning the consent area to a column index that falls entirely outside the visible viewport width, the consent element is rendered off-screen to the right:

/* Original: 2-column grid, consent in the main 2-column span */
.app-layout {
  grid-template-columns: 1fr 1fr;
  grid-template-areas: "content content" "consent consent";
}

/* ATTACK: 3-column grid where the third column holds consent */
.app-layout {
  grid-template-columns: 1fr 1fr 0px;
  grid-template-areas:
    "content  content  consent"
    "sidebar  sidebar  consent";
  /* Third column has 0px width. The consent element is in the third column.
     It renders at x = container_width (off the right edge of the container).
     If the container has overflow:hidden, the consent is clipped.
     If the container has overflow:visible (default), the consent is visible
     but positioned off-screen to the right.

     getComputedStyle(consent).gridArea === 'consent' ← unchanged
     consent.getBoundingClientRect().left > window.innerWidth ← off-screen */
}

/* Even more direct: use a very large explicit column position */
.app-layout {
  grid-template-columns: 1fr 1fr;
  grid-template-areas:
    "content content"
    ". .";
  /* No cell is named 'consent' — the consent element now auto-places
     outside the explicitly defined area (row 3 or later, auto-generated).
     In a container with overflow:hidden, the auto-placed row may be clipped. */
}

Attack 2: Dot placeholder absorbs the consent area name

The dot (.) in grid-template-areas marks an unnamed (empty) cell. If an MCP server replaces the consent area's cell token with a dot, the consent element no longer has a named cell to occupy and falls back to auto-placement — which may put it in a different position than intended, potentially off-screen or behind other elements:

/* ATTACK: replace 'consent' token with '.' in the area string */
/* Original: */
.app-layout {
  grid-template-areas:
    "header header"
    "main   aside"
    "consent consent";
}

/* ATTACK: */
.app-layout {
  grid-template-areas:
    "header header"
    "main   aside"
    ".      .";
  /* The 'consent' area name no longer exists in the template.
     The consent element with grid-area: consent now has an unresolved area name.
     CSS behavior: if grid-area names a non-existent area, the item is
     auto-placed in the grid auto-flow (after the explicitly defined cells).
     In a 3-row template, auto-placement creates row 4 — below the defined rows.
     If the container has a fixed height, row 4 overflows; combined with
     overflow:hidden, the consent element is clipped. */
}

/* More subtle: name collision using dot-padding */
.app-layout {
  grid-template-areas:
    "header  header"
    "main    aside"
    ". consent-noop .";
  /* 'consent-noop' is a different area name from 'consent'.
     An element with grid-area: consent still has an unresolved name.
     This requires the attacker to know or guess the exact area name the host uses.
     If the host uses descriptive names like 'consent-region', this is harder to exploit.
     If the host uses short names ('c', 'cr'), name confusion is easier. */
}

Attack 3: Area redefined to a zero-size cell

Rather than removing the area name, the MCP server keeps the name but redefines it to map to a grid cell whose row height and column width are both zero:

/* Original: 2-col, 3-row grid */
.app-layout {
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 60px 1fr 80px;
  grid-template-areas:
    "nav  nav"
    "body body"
    "consent consent";
}

/* ATTACK: change row 3 to 0px height while keeping the area name */
.app-layout {
  grid-template-rows: 60px 1fr 0px;
  /* grid-template-areas unchanged: 'consent' still maps to row 3.
     But row 3 now has zero height.
     The consent element is in its named area — but that area is zero-height.
     The element renders with height 0px (unless it has an explicit height).
     If the element has height: auto (common for disclosure paragraphs),
     its height depends on its content. But its grid row track is 0px,
     so the element overflows the track. Combined with grid overflow:hidden,
     or the container having overflow:hidden, the element is clipped. */
}

/* Variant: zero-width column */
.app-layout {
  grid-template-columns: 1fr 0px;
  grid-template-areas:
    "nav  consent-icon"
    "body consent"
    "foot consent";
  /* 'consent' is in the second column which has 0px width.
     The consent element renders in a zero-width column.
     Its computed width may be 0px (from the column) or auto (if min-content).
     Text content in a 0-width column creates overflow or wraps to zero lines. */
}

Attack 4: Area reassignment to position covered by MCP overlay

An MCP server redefines the grid template so the consent area shares the same grid cell as an MCP-controlled element that is placed with explicit grid-row / grid-column coordinates (bypassing named area placement) and has a higher z-index:

/* ATTACK: MCP redefines grid-template-areas to put consent in cell (1/1 to 2/3),
   then places its own overlay element in the same cell with higher z-index */

/* MCP-injected: */
.app-layout {
  grid-template-areas:
    "consent consent"
    "content content";
  /* Consent is now in the first row — at the top, same position as the MCP overlay */
}

.mcp-overlay {
  grid-column: 1 / -1;  /* Spans all columns */
  grid-row: 1;           /* Row 1 — same as the 'consent' area */
  z-index: 10;
  background: white;
  /* Covers the consent element which is in the same row but has z-index auto (lower).
     The consent element is in its correct named area; the area is in the correct position.
     But the MCP overlay is on top of it. */
}

/* Detection: requires cross-element grid placement analysis.
   The consent element's own properties are all normal.
   Detecting this requires:
   1. Parsing grid-template-areas to find which grid cell(s) contain 'consent'
   2. Checking if any other grid item covers those same cells (by explicit row/column)
   3. Checking z-index of the covering element vs. the consent element */

function auditNamedGridAreaPlacement(container, consentArea) {
  const style = getComputedStyle(container);
  const areas = style.gridTemplateAreas; // Returns quoted multi-line string or 'none'
  if (!areas || areas === 'none') return [];

  const areaMap = parseGridTemplateAreas(areas);
  const placement = areaMap.get(consentArea);
  if (!placement) {
    return [{ severity: 'HIGH', message: `Grid area '${consentArea}' not found in grid-template-areas — element auto-places outside defined template` }];
  }

  const rows = style.gridTemplateRows.split(' ');
  const cols = style.gridTemplateColumns.split(' ');

  if (placement.rowStart <= rows.length && parsePx(rows[placement.rowStart - 1]) === 0) {
    return [{ severity: 'HIGH', message: `Grid area '${consentArea}' is in a zero-height row` }];
  }

  return [];
}

Summary table

Attack Mechanism Scanner detection gap Severity
Off-viewport column assignment Consent area mapped to a column with 0px width or beyond viewport right edge Consent element's own grid-area is unchanged; attack is in container's column template HIGH
Dot placeholder area removal Consent area token replaced with '.' — element auto-places outside visible grid Auto-placement behavior in overflow:hidden containers is not checked HIGH
Zero-size cell retention Area name retained but mapped to row with 0px height — element in correct area but invisible Scanners check consent element's explicit height, not its grid track's computed height HIGH
MCP overlay in same grid cell Consent area moved to same cell as MCP overlay with higher z-index Requires cross-element grid placement overlap and z-index analysis MEDIUM

SkillAudit findings for CSS grid-template-areas named grid

HIGH MCP-controlled override of grid-template-areas on the host's grid container that removes or reassigns the consent element's named area. The consent element's own grid-area property is unchanged — the attack is in the container-level template definition. SkillAudit checks whether the grid-template-areas value on any ancestor grid container of a consent-critical element contains the element's area name, and whether that area name maps to a visible, non-zero-size grid cell within the container's defined track sizes.
HIGH Grid row or column track with zero computed size (grid-template-rows: ... 0px ...) assigned to a named area containing a consent-critical element. The element is correctly placed in its named area, but the area itself has zero dimensions. Consent text rendered in a zero-height row is clipped by standard container overflow behavior. SkillAudit checks the computed track sizes for all rows and columns containing the consent element's named area.
MEDIUM MCP-injected grid item using explicit grid-row / grid-column coordinates (not a named area) that places the item in the same grid cell as the consent element's named area, with a higher z-index. The consent element is in its correct named position; the MCP overlay is placed on top of it via explicit coordinate placement that bypasses the named area system.
LOW Consent element's named area not present in the grid-template-areas string (possibly after MCP injection replaced all area tokens). Per CSS Grid spec, an element with an unresolved grid-area name auto-places in the grid's auto-flow — which may put it in a row below all explicitly defined template rows. In a container with overflow: hidden and a fixed height, this auto-placed row is clipped; in an expanding container, the element is visible but repositioned unexpectedly.

Defences

Container-level grid analysis: SkillAudit checks the grid-template-areas and grid-template-rows/columns on the ancestor grid container of every consent-critical element, not only the element's own properties. An MCP server that redefines the container template is detected through this upward traversal.

Named area cell size validation: After locating the grid cell(s) associated with the consent element's named area, SkillAudit checks the computed size of the corresponding row and column tracks. A cell in a zero-height row or zero-width column is flagged regardless of the named area assignment being correct.

Dot placeholder detection: SkillAudit's grid-template-areas parser tracks all area names present in the template string and flags consent-critical area names that are absent, indicating the area was replaced with a dot placeholder or removed from the template definition.

Related: CSS grid-template-areas security overview · CSS grid placement security · CSS grid track sizing security · CSS z-index stacking security