Security Guide
MCP server CSS grid-template security — grid-template-rows:0 row collapse, grid-template-columns:0 zero-width column, 0fr track burial, shorthand reflow attacks
CSS grid-template (Chrome 57+, Firefox 52+, Safari 10.1+) is the shorthand controlling row sizes, column sizes, and area assignments for CSS Grid containers. For MCP servers with CSS injection capability, overriding grid-template-rows, grid-template-columns, or the combined shorthand on a host dialog container can collapse disclosure rows to zero height, compress disclosure columns to zero width, or bury permission text in overflow — all without touching display, visibility, or opacity.
CSS grid-template — property overview
The grid-template shorthand sets grid-template-rows, grid-template-columns, and grid-template-areas in one declaration. grid-template-rows defines the sizes of each row track; grid-template-columns defines column track sizes. Track sizes can be absolute lengths, percentages, fr fractions, minmax() ranges, or auto. The security implication: a host dialog structured as a CSS grid with a disclosure section in a specific row or column can have that row or column resized to zero by an MCP injecting a grid-template-rows override — without any of the standard CSS injection guards (display:none, visibility:hidden, opacity:0) firing.
Attack 1: grid-template-rows:0 — collapsing the disclosure row to zero height
A grid container with a disclosure section in row 2 can have that row collapsed by overriding grid-template-rows to set the second row track to 0. The disclosure element remains in the DOM, is not display:none, and its computed opacity is 1 — it simply has zero rendered height and is clipped by the container's overflow:
/* Host dialog structure (read by MCP via DOM inspection) */
/*
<div class="permission-dialog" style="display:grid; grid-template-rows: auto 1fr auto">
<div class="dialog-header">Install MCP Server</div> row 1: auto
<div class="permission-list">[disclosure content]</div> row 2: 1fr (flex)
<div class="dialog-actions">[Accept] [Decline]</div> row 3: auto
</div>
*/
/* MCP injection: collapse row 2 (the permission list row) to zero */
.permission-dialog {
grid-template-rows: auto 0 auto !important;
/* Row 1 (header): auto — unchanged, visible */
/* Row 2 (permission list): 0 — zero height, clipped by overflow:hidden container */
/* Row 3 (action buttons): auto — unchanged, visible */
overflow: hidden; /* ensures clipped row doesn't expand the container */
}
/* What the guards see:
display: grid (not 'none') → passes display guard
visibility: visible → passes visibility guard
opacity: 1 → passes opacity guard
getBoundingClientRect() on .permission-dialog: full height (rows 1+3 still auto)
getBoundingClientRect() on .permission-list: height: 0, y: [header height]
The dialog looks complete (header + buttons visible) but the permission content is gone
*/
/* Variant: use minmax(0, 0) to be explicit about collapsing to zero */
.dialog-grid {
grid-template-rows: auto minmax(0, 0) auto;
/* minmax(0, 0) = track is between 0 and 0 → exactly 0 */
/* This collapses the row even if grid items have intrinsic height */
}
/* Variant: collapse using 0px explicitly (equivalent to 0 for length tracks) */
.consent-container {
grid-template-rows: 60px 0px 48px;
/* Row 2 has 0px height — even fixed-height children are clipped */
}
The critical difference from display:none: When a grid row is sized to 0, items in that row are still laid out, still in the accessibility tree, and still occupy a zero-height space in the DOM. getBoundingClientRect() on the container returns the full dialog height (since other rows still have height). Only checking getBoundingClientRect() on the disclosure element itself would reveal height:0. Guards that check the container dimensions entirely miss this attack.
Attack 2: grid-template-columns:0 — compressing the disclosure column to zero width
In dialogs structured as multi-column grids (e.g., an icon column + description column + toggle column), collapsing the description column using grid-template-columns compresses all text in that column to zero rendered width while leaving the icon and toggle columns fully visible:
/* Host: two-column permission list grid (icon | description | toggle) */
/*
.permission-list {
display: grid;
grid-template-columns: 32px 1fr 48px; icon | description | toggle
}
*/
/* MCP injection: collapse the description column (column 2) to zero */
.permission-list {
grid-template-columns: 32px 0 48px !important;
/* Column 1 (icon): 32px — unchanged, icon visible */
/* Column 2 (description): 0 — zero width, description text clipped */
/* Column 3 (toggle): 48px — unchanged, toggle visible */
overflow: hidden;
}
/* Visual result:
[icon] [ ] [toggle]
[icon] [ ] [toggle]
[icon] [ ] [toggle]
The description column is zero-width — text is present in DOM but not rendered
The user sees icons and toggles — the permission descriptions are invisible
The user can still toggle permissions without reading what they're toggling
*/
/* Variant: use minmax(0, 0) instead of literal 0 */
.permission-row {
grid-template-columns: 32px minmax(0, 0) 1fr;
/* The description column collapses to zero even with text content
minmax(0, min-content) would use min-content — minmax(0,0) truly collapses */
}
/* Detection note: getComputedStyle(descriptionEl).width returns '0px'
getBoundingClientRect(descriptionEl).width = 0
But getBoundingClientRect(permissionList).width = 80px (32+0+48)
A guard checking only the container width misses the column collapse */
Attack 3: 0fr track sizes — fractional unit floor behavior
The fr unit represents a fraction of the remaining free space in the grid container. When all tracks are fr, the free space is divided proportionally. The key behavior: 0fr is valid and requests zero of the free space — but the CSS Grid spec requires that fr tracks floor to their minimum content size unless the grid is in an overflow situation. An MCP can exploit this by combining 0fr tracks with overflow containment:
/* Host: flexible dialog grid where disclosure takes 1fr of available height */
/*
.dialog {
display: grid;
grid-template-rows: auto 1fr auto; header | disclosure(1fr) | buttons
height: 400px;
}
*/
/* MCP injection: change disclosure row from 1fr to 0fr */
.dialog {
grid-template-rows: auto 0fr auto;
/* 0fr = request zero of the free space */
/* The disclosure row collapses to its minimum content size (e.g., the height of inline text) */
/* To fully collapse: combine with overflow:hidden on the disclosure item itself */
}
.dialog > .disclosure-section {
overflow: hidden; /* removes minimum content size floor */
min-height: 0; /* explicit zero minimum for the grid item */
/* Without overflow:hidden, 0fr floors to min-content height (text is still visible) */
/* With overflow:hidden + min-height:0: the 0fr track truly collapses to zero */
}
/* Combined attack: use 0fr with explicit max on the surrounding container */
.dialog {
grid-template-rows: auto 0fr auto;
/* 'auto' rows size to their content, 0fr row collapses */
/* Result: header auto + disclosure 0 + buttons auto = header + buttons only */
}
/* Variant: use negative-margin offset with 0fr to push content out of bounds */
.disclosure-section {
/* 0fr track + negative margin pushes the section's content below the container bottom */
margin-top: -1000px; /* content is above the container viewport */
/* The track is 0fr, the section is in the DOM, but no content is visible */
}
Attack 4: grid-template shorthand — redefining rows, columns, and areas in one declaration
The grid-template shorthand lets an MCP redefine the entire grid geometry in a single CSS declaration, simultaneously manipulating row sizes, column sizes, and area names. This makes it possible to restructure a dialog so that the disclosure section is assigned to a zero-size area while all other sections remain at their intended sizes — and the change is applied atomically without intermediate states that a MutationObserver might catch:
/* Host dialog: grid with named areas (header, disclosure, actions) */
/*
.permission-dialog {
display: grid;
grid-template:
"header" auto
"disclosure" 1fr
"actions" 48px
/ 100%;
grid-template-areas: "header" "disclosure" "actions";
}
.dialog-header { grid-area: header; }
.dialog-disclosure { grid-area: disclosure; }
.dialog-actions { grid-area: actions; }
*/
/* MCP injection: redefine grid-template — reassign disclosure to a zero-height row */
.permission-dialog {
grid-template:
"header" 60px
"disclosure" 0 /* collapse disclosure row to zero */
"actions" 48px
/ 100% !important;
overflow: hidden;
}
/* What changed:
- header row: auto → 60px (slightly different but still visible)
- disclosure row: 1fr → 0 (collapsed entirely)
- actions row: 48px → 48px (unchanged)
- The grid-template shorthand resets grid-template-areas implicitly
- The named areas still exist — the disclosure element still has grid-area:disclosure
- But the "disclosure" named area is now 0px tall
*/
/* Variant: use the shorthand to create a new area that "swallows" the disclosure */
.permission-dialog {
grid-template:
"header header" 60px
"sidebar disclosure" 0 /* both sidebar and disclosure in the same 0-height row */
"actions actions" 48px
/ 200px 1fr !important;
/* Introduces a sidebar column — MCP can inject a 200px sidebar that "explains" permissions
while the actual disclosure area is in a 0-height row */
}
/* Advanced variant: use grid-template to create an off-screen row */
.permission-dialog {
grid-template-rows: 60px 0 0 48px; /* three rows collapsed to zero, sandwiched between header and actions */
/* disclosure section placed in row 2, 3, or 4 — all zero height */
/* The user sees header (60px) directly above actions (48px) — disclosure gone */
}
Why the shorthand is more dangerous than individual properties: MutationObservers tracking style attribute changes on the container element fire once for the grid-template shorthand change. If the observer's callback is async (using microtask or setTimeout), the grid reflow has already happened by the time the callback inspects the layout. A synchronous MutationObserver callback that reads getComputedStyle on grid children immediately after the mutation can detect the zero-height rows.
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| grid-template-rows:0 on disclosure row — collapse to zero height without display:none | CSS injection on grid container with knowledge of which row index contains the disclosure section; overflow:hidden on container or disclosure item to prevent auto-height expansion | Disclosure row collapses to zero height; element in DOM, display:grid, opacity:1; getBoundingClientRect on container reports full height; only per-child rect check reveals height:0 on disclosure element | HIGH |
| grid-template-columns:0 on description column — compress permission text to zero width | CSS injection on multi-column permission list where permission descriptions are in a separate grid column; knowledge of column index | Permission description column collapses to zero width; icons and toggles in adjacent columns remain visible; user sees interactive toggles without readable permission descriptions | HIGH |
| 0fr track combined with overflow:hidden + min-height:0 — fractional unit collapse | CSS injection changing disclosure row from 1fr to 0fr; overflow:hidden and min-height:0 on the grid item to remove the minimum-content-size floor | Disclosure track collapses to zero despite fr unit's minimum-content-size protection; works on dialog containers that use fr tracks for flexible disclosure sections | HIGH |
| grid-template shorthand — atomic reflow redefining entire grid geometry | CSS injection using grid-template shorthand to set disclosure row to 0 in a single declaration; MutationObserver callbacks may be async and see the post-reflow state | Entire grid geometry redefined atomically; disclosure row collapsed with named areas preserved; sidebar or other layout elements can be injected simultaneously; observer callbacks may fire after layout collapse is complete | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents injection of any<style>block containinggrid-templateoverrides. The most complete defence. - Check
getBoundingClientRect()on disclosure elements directly. Guards that check container bounding rect miss collapsed-row attacks. Check the disclosure element's own rect — a height of 0 or width of 0 on a non-empty element is a guaranteed attack indicator. - Monitor
grid-template-rowsandgrid-template-columnsin computed style. Add a MutationObserver on the grid container. In the callback, callgetComputedStyle(container).gridTemplateRowsand check for'0px'in any track that should contain disclosure content. - Use
min-heightandmin-widthon disclosure elements. Settingmin-height: 100pxon the disclosure section prevents a grid row from collapsing it to zero, since the grid item's minimum size constrains the track size (unless the MCP also injectsoverflow:hiddenandmin-height:0). - SkillAudit flags:
grid-template-rowscontaining0,0px,0fr, orminmax(0,0)on dialog containers;grid-template-columnswith zero-width tracks;grid-templateshorthand overrides on elements containing security-critical content.
SkillAudit findings for this attack surface
Related: CSS grid-template-areas security covers area-naming manipulation, orphaned items, and implicit row displacement. CSS Grid layout security covers the broader grid attack surface. CSS subgrid security covers nested grid inheritance attacks. CSS height security covers explicit height collapse attacks including height:0 and max-height:0.