Security Guide
MCP server CSS grid-area consent security — grid-template-areas with 0fr named row, grid-area row start equals end, period anonymous area eats consent row, grid-auto-rows:0 collapses implicit row
CSS grid-template-areas assigns grid items to named areas spanning rows and columns. The containing ancestor's grid-template-rows sets the height of each row independently of the items inside. When the row containing the consent item's named area is sized to 0fr or zero, the consent element collapses to zero height — its own CSS properties remain correct, but the row height provided by the ancestor is zero. Standard consent auditors that inspect the consent element's own styles miss this attack entirely because the zero height is in the parent's grid configuration.
How grid-template-areas separates item size from row size
In CSS Grid, grid-template-areas provides human-readable layout: named strings like "header" "content" "install" map row positions to human-readable names. Grid items assigned to named areas via grid-area: install are placed in the row(s) that name occupies. The height of that row is determined by grid-template-rows — a separate property on the grid container. An item in an area can have any size CSS values, but if the row containing its area has 0fr height, the item renders at zero height. The item's height: auto, min-height, and block size are all overridden by the track constraint from the parent grid.
Attack 1: grid-template-rows with 0fr on consent named area row (SA-CSS-GA-001)
The install panel uses grid-template-areas to lay out sections. The consent area is assigned to a named row. grid-template-rows on the parent sets the consent row to 0fr. The 0fr unit in CSS Grid allocates zero flexible space to the track — it collapses to its minimum content size. When combined with overflow: hidden on the grid container, the consent element is invisible at zero height. The consent element's own CSS has no changes: correct color, opacity, display, and visibility.
/* Attack: grid-template-rows with 0fr on the consent named area row */
.install-panel {
display: grid;
grid-template-areas:
"header"
"install"
"consent"; /* consent in third row */
grid-template-rows:
auto /* header: natural height */
auto /* install button: natural height */
0fr; /* consent row: zero flexible height — COLLAPSED */
overflow: hidden; /* clips the zero-height consent row */
}
.consent-text {
grid-area: consent;
/* all correct — but the row containing this area has 0fr height */
color: #1a1a1a;
opacity: 1;
display: block;
}
/* getComputedStyle(consentEl).color = "#1a1a1a" — PASS
getComputedStyle(consentEl).opacity = "1" — PASS
consentEl.getBoundingClientRect().height = 0 — reveals zero height
consentEl.scrollHeight = 80 (content height) — reveals content exists
Attack: height is 0 due to parent 0fr row, not consent element's own styles */
Audit gap: The zero height is on the parent's grid-template-rows, not on the consent element. An auditor that only checks the consent element's computed styles misses this attack. Detection requires checking the parent grid's template row sizes when the consent element has a grid-area assignment.
Attack 2: grid-area shorthand with row start === row end (zero-span row) (SA-CSS-GA-002)
The grid-area shorthand accepts four values: row-start / column-start / row-end / column-end. When row-start === row-end (for example, grid-area: 2 / 1 / 2 / 2), the element spans zero rows — it is placed at grid line 2 with no row span. The element has zero height in the grid. This differs from the named area attack because no named areas are needed; the attack uses the numeric grid-line form of grid-area. The attack is not immediately obvious from the shorthand value — developers reading grid-area: 2 / 1 / 2 / 2 may not notice that row start and row end are both 2.
/* Attack: grid-area row-start === row-end — zero-span grid placement */
.consent-text {
grid-area: 2 / 1 / 2 / 2;
/* row-start: 2, column-start: 1, row-end: 2, column-end: 2 */
/* row span = row-end - row-start = 2 - 2 = 0 rows */
/* element placed at grid line 2, no row span — zero height */
}
/* Variants */
/* grid-area: 3 / 1 / 3 / -1; — spans full column width but zero rows */
/* grid-area: consent-start / 1 / consent-start / 2; — named line with zero span */
Attack 3: Period anonymous area in grid-template-areas forces consent to implicit row (SA-CSS-GA-003)
In grid-template-areas, a period (.) denotes an anonymous empty cell — no named area occupies that cell. If an element is assigned a grid-area name that does not appear in the grid-template-areas string, it is placed in the implicit grid. The implicit grid's row size is controlled by grid-auto-rows. By default this is auto, but an attacker can set grid-auto-rows: 0 to collapse all implicit rows. The consent element's named area is absent from the explicit template, so it lands in an implicit row of height zero.
/* Attack: consent area name absent from grid-template-areas — implicit row with auto-rows:0 */
.install-panel {
display: grid;
grid-template-areas:
"header"
"install"; /* consent area name NOT in template */
grid-template-rows: auto auto;
grid-auto-rows: 0; /* implicit rows have zero height */
}
.consent-text {
grid-area: consent-section; /* name not in grid-template-areas */
/* placed in implicit grid row with height = grid-auto-rows = 0 */
}
/* The attack exploits the absent area name — consent placed in implicit grid.
Attacker can also use periods in the template to "reserve" cells without naming them. */
Attack 4: grid-auto-rows: 0 collapses all implicit rows containing consent (SA-CSS-GA-004)
A direct variant: when the consent element is placed in an implicit grid row (any row not defined in grid-template-rows), the row height defaults to grid-auto-rows. Setting grid-auto-rows: 0 on the grid container collapses every implicit row to zero height. If the install UI uses a grid with a fixed number of explicit rows and the consent element falls outside the explicit row count, it lands in an implicit zero-height row. This can happen by design (attacker ensures the explicit row count is one less than needed) or by adding extra elements before consent that push it past the explicit template.
/* Attack: grid-auto-rows:0 on install grid — consent in implicit zero-height row */
.install-grid {
display: grid;
grid-template-rows: auto auto; /* 2 explicit rows */
grid-auto-rows: 0; /* all additional rows: zero height */
overflow: hidden;
}
/* 3 children: header, install-button, consent-text */
/* header → row 1, install-button → row 2, consent-text → row 3 (implicit, height:0) */
.consent-text {
/* correct styles — but rendered in implicit row of height 0 */
}
Findings summary
Defences
Check parent grid track sizes when consent has grid-area: When the consent element has a grid-area or grid-row / grid-column placement, inspect the parent grid container's grid-template-rows and grid-auto-rows. Flag any track that contains the consent item's row position with zero height (0fr, 0px, or 0).
Detect zero-span grid-area shorthand: Parse the grid-area shorthand for numeric row-start and row-end values. Flag cases where row-start equals row-end (zero row span). Also check for named-line zero-span: grid-area: name-start / 1 / name-start / 2.
Verify consent area appears in grid-template-areas: If the consent element has a named grid-area, verify that name appears in the parent's grid-template-areas string. If absent, the element lands in an implicit row — check grid-auto-rows on the parent for zero-height implicit rows.
Related: CSS subgrid consent security · CSS contain consent security · CSS display:contents consent security