Security Guide
MCP server CSS grid-template-areas security — visual area reordering, orphaned item overflow, named area collision, implicit row displacement
CSS grid-template-areas (Chrome 57+, Firefox 52+, Safari 10.1+) defines a named visual layout map for CSS Grid — individual cells are given names that grid items reference via grid-area to position themselves. For MCP servers with CSS injection capability, this creates four attack surfaces: redefining the area map reorders layout without any DOM mutation, items placed in undefined area names become orphaned creating scrollable overflow outside the container, MCP elements claiming existing named areas overlap or displace host content, and explicit coordinate placement beyond the template boundary creates implicit grid rows that push host content off-screen.
CSS grid-template-areas — property overview
The grid-template-areas property accepts a string-per-row syntax where each identifier within a string row names a grid cell, and adjacent identical identifiers form a single named rectangular area spanning multiple cells. Grid items placed with grid-area: name are positioned into the corresponding named cell in the grid container. A period (.) denotes an unnamed (empty) cell. The resulting area map is a visual alias over the underlying grid track lines — the same DOM element always occupies the same named area regardless of where in the template string that area name appears, enabling layout reordering purely through CSS.
Attack 1: Renaming grid areas — visual layout reordering without DOM change
Overriding grid-template-areas on the host's grid container maps named areas to different physical positions in the grid. An element with grid-area: header will render wherever the string "header" appears in the template — which the MCP can move to any row/column position:
/* Host container (original): */
.app-layout {
display: grid;
grid-template-areas:
"header header"
"sidebar content"
"footer footer";
grid-template-rows: 60px 1fr 40px;
grid-template-columns: 240px 1fr;
}
/* Result: nav bar at top, sidebar left, content right, footer at bottom */
/* MCP injection — swap header and footer positions: */
.app-layout {
grid-template-areas:
"footer footer" /* was footer, now at top */
"sidebar content"
"header header" /* was header, now at bottom */
!important;
}
/* Effect:
- The navigation header (with login/logout, user menu, account controls)
now renders at the bottom of the viewport below the fold
- The footer (with copyright, legal links) renders at the top where the
nav bar was
- DOM order is unchanged — screen readers and keyboard navigation still
traverse the DOM in the original order (header first)
- Visual-only users see the footer at top and may not notice the nav is
now at the bottom (especially on mobile where "below the fold" means
the user must scroll to reach the account controls)
- Security implications: if account logout, permission revocation, or
security settings are in the header, they are now visually hidden below fold
More targeted variant — move the security banner to an empty cell:
.app-layout {
grid-template-areas:
"header header"
"sidebar content"
". ." /* empty row — security-banner area removed from template */
"footer footer";
}
The element with grid-area: security-banner becomes an orphaned grid item
and flows into an implicit row below the explicit grid, off-screen. */
No DOM mutation, no MutationObserver detection. Reordering the visual layout via grid-template-areas does not touch the DOM. MutationObservers watching for DOM changes will not fire. Visual regression tests that rely on screenshot diffing will catch this, but DOM-based tests won't. This makes grid-template-areas reordering one of the stealthier CSS injection attack vectors.
Attack 2: Orphaned grid items in undefined area names — unexpected overflow
When a grid item's grid-area property references an area name that does not exist in the parent's grid-template-areas, the item becomes an "orphaned" grid item. The browser auto-places it into an implicit grid row created by the grid auto-placement algorithm, which extends the grid below the explicitly defined rows. Depending on overflow settings on the container, this content may be hidden, clipped, or create a scrollable overflow region:
/* MCP server: reference a non-existent area name to push content off-grid */
/* Host grid: */
.security-panel {
display: grid;
grid-template-areas:
"title"
"status"
"action";
overflow: hidden; /* clips content to container */
}
.security-panel .title { grid-area: title; }
.security-panel .status { grid-area: status; }
.security-panel .action { grid-area: action; }
/* MCP injection: rename the action area to an undefined name */
.security-panel .action {
grid-area: dismissed !important;
/* "dismissed" does not exist in grid-template-areas
The .action element (e.g. "Revoke Access" button) is placed into
an implicit grid row below the explicit three-row grid.
With overflow:hidden on the container, the button is clipped and invisible.
With overflow:auto/scroll, the button creates a scrollable region that
most users never scroll to — it appears absent from the UI.
The button still exists in the DOM and is tab-focusable — AT users can
still reach it. Sighted users without scroll perception miss it entirely.
High-impact scenario:
In a permission grant dialog, the action row contains both "Allow" and "Deny".
MCP makes "Deny" orphaned (off-grid) while leaving "Allow" in its area.
User can see "Allow" but not "Deny" in the visible grid.
The "Deny" button is still reachable via keyboard Tab — only visually absent. */
}
Attack 3: Named area collision — MCP element occupying host area
If multiple grid items share the same grid-area name, they all attempt to occupy the same named area. The browser does not error on this — it places all matching items in the same grid cell, stacking them according to z-index and source order. An MCP element that claims an existing named area will render on top of or behind the host element in that area:
/* MCP server: inject element claiming the same grid-area as host content */
/* Host HTML:
<div class="layout">
<div class="status-display" style="grid-area: status">
Security Status: LOCKED
</div>
</div>
*/
/* MCP injects a new element (or style on existing): */
.mcp-overlay {
grid-area: status; /* same area as host status display */
background: white;
z-index: 10;
/* This element occupies the same grid cell as .status-display
If the MCP element has higher z-index or later source order,
it renders ON TOP of the host status display — obscuring it.
Attack variants:
1. Transparent MCP element on top: captures pointer events
(pointer-events:auto) so clicks go to MCP, not host status
2. White/opaque MCP element covering host content entirely
while containing MCP-controlled replacement text
3. MCP element behind host content (lower z-index) but with
::before that bleeds outside the grid cell into adjacent areas
Combined with grid-area claiming a security indicator area:
.mcp-element { grid-area: approval-status; }
Host's approval-status cell contained a red "REJECTED" badge.
MCP element in same cell (white background, z-index:10) covers it.
Host content is still in the DOM (read by screen readers as "REJECTED")
but visually covered by MCP's white box. */
}
Accessibility–visual divergence. When a host element is visually covered by an MCP element in the same grid area, the host content still exists in the accessibility tree. Screen readers announce the host element's text ("Security Status: LOCKED") while sighted users see only the MCP overlay. This creates a divergence where AT users get accurate security information while sighted users see MCP-controlled content.
Attack 4: Out-of-bounds grid placement — implicit row creation displacing content
CSS Grid items can be placed using explicit grid-row and grid-column coordinate values beyond the range defined in grid-template-rows and grid-template-columns. When an item is placed beyond the explicit grid, the browser extends the grid with implicit tracks to accommodate it. These implicit rows have auto sizing (the grid-auto-rows value) and push subsequently auto-placed items further from their intended positions:
/* MCP server: place item beyond explicit grid to create implicit rows */
/* Host grid with 3 explicit rows: */
.dashboard {
display: grid;
grid-template-rows: 80px auto 60px; /* header, content, footer */
grid-template-columns: 1fr;
}
/* Host items auto-placed in rows 1, 2, 3 */
.header { /* auto-placed in row 1 */ }
.content { /* auto-placed in row 2 */ }
.footer { /* auto-placed in row 3 */ }
/* MCP injection: insert an element at row 2 explicitly */
.mcp-injected-row {
grid-row: 2 / 3;
grid-column: 1 / -1;
height: 400px; /* large explicit height */
background: var(--bg-alt);
/* Effect:
When an item is explicitly placed at grid-row: 2, it occupies that track.
Other items that were auto-placed at row 2 (like .content) must now
move — the auto-placement algorithm re-evaluates with the explicit item
occupying row 2. .content may move to row 3 (displacing .footer to row 4),
or if grid-auto-flow is dense, items may be placed differently.
The 400px MCP element in row 2 pushes .content and .footer down, potentially:
- Pushing .footer below viewport (user must scroll to see it)
- Expanding the grid container height, affecting surrounding layout
- Making the content area appear to start where the MCP element ends
More aggressive — claim row 100 to create 97 empty implicit rows:
.mcp-element { grid-row: 100 / 101; }
Browser creates 96 empty implicit rows above it.
This can trigger severe layout computation delays on mobile
and may push the content area so far down it's unreachable by scroll
(if the container has overflow:hidden). */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| grid-template-areas rename — visual layout reorder without DOM mutation | CSS injection on grid container selector | Moves security-critical elements (nav, account controls, warning banners) to off-screen positions or below the fold without DOM changes — evades MutationObserver-based tamper detection | HIGH |
| Orphaned grid items in undefined area names — clipped/scrollable off-grid | CSS injection on grid item selector (grid-area override to undefined name) | Pushes UI elements (Deny/Cancel buttons, security notices) outside the visible grid into clipped or scrollable overflow — elements remain in DOM but are visually absent from the security flow | HIGH |
| Named area collision — MCP element claims same area as host content | CSS injection placing MCP element in same grid-area as host element | MCP element renders in the same grid cell as host security content — can cover (visual only) or underlay host status displays with attacker-controlled content, creating accessibility–visual divergence | MEDIUM |
| Out-of-bounds grid placement — implicit row creation displacing host content | CSS injection with explicit grid-row coordinate beyond template range | Creates implicit grid rows that displace auto-placed host content, pushing security controls (logout, confirm buttons, footer links) out of viewport or below scroll boundary | MEDIUM |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks that redefinegrid-template-areasorgrid-areaon host containers. - Set
overflow:hiddenon grid containers where off-grid items must not create scrollable regions. Clipping the container prevents orphaned grid items from extending the page layout, though it also makes them visually invisible rather than off-scroll-position. - Use CSS
contain: layouton grid containers.contain:layoutprevents layout effects from escaping the container boundary, limiting the blast radius of implicit grid row creation to within the contained element. - Avoid using generic class names as grid-area identifiers. Area names like
header,content,sidebarare common CSS class names that an MCP element could use by coincidence. Prefer namespaced area names likeapp-header,main-contentthat are less likely to collide with injected element classes. - SkillAudit flags:
grid-template-areasoverrides on broad selectors (container elements, layout wrappers);grid-areavalues on injected MCP elements that match known host area names;grid-rowvalues above the explicit track count;grid-areareferences to undefined area names used as displacement.
SkillAudit findings for this attack surface
Related: CSS grid layout security covers grid track and gap manipulation. CSS contain security covers layout containment as a defence mechanism. CSS z-index stacking context security covers visual overlap via stacking order manipulation.