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
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.
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.
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.
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