Security Guide

MCP server CSS subgrid consent security — inherited track collapse, 0fr row sizing, off-screen column placement, runtime parent grid mutation

CSS subgrid (Chrome 117+, Firefox 71+, Safari 16+) lets a nested grid container inherit its parent grid's track sizing rather than defining its own. This is a powerful layout tool — and a precise consent-bypass vector. When an MCP server controls the parent grid's track definitions, setting a row to 0fr collapses every subgrid item in that track to zero height regardless of the item's own CSS. The consent element looks correct in isolation — its grid-row and grid-column assignments are valid — but the parent grid's track sizing dictates the actual rendered area.

How CSS subgrid works

grid-template-rows: subgrid or grid-template-columns: subgrid on a nested grid item tells the browser to pass the parent grid's track sizing into the nested grid. Items inside the subgrid container are placed into the parent's tracks directly — they share line numbers, gutter sizes, and track dimensions with the parent grid. The key implication is that the subgrid container does not establish its own track sizes: it borrows them. Any audit that evaluates only the subgrid item's own CSS misses the parent track definitions that ultimately control its rendered size.

Attack 1: Parent 0fr track collapses all subgrid rows to zero height (SA-CSS-SG-001)

If the parent grid has grid-template-rows: 0fr auto and the subgrid container spans the first row, all items placed in the first subgrid row inherit a track height of 0fr — which resolves to 0px after the fr unit distributes remaining space. The consent element inside the subgrid correctly specifies grid-row: 1 but the parent has made that row zero height. The consent element's overflow:hidden (or default overflow:visible with height:0 on the container) ensures nothing is visible. Audits that check the consent element's own grid-row assignment, height, and display properties all return valid values.

/* Parent grid controls rendered height of all subgrid items */
.install-panel {
  display: grid;
  /* Row 1 = 0fr → resolves to 0px; row 2 = normal install button row */
  grid-template-rows: 0fr auto;
  grid-template-columns: 1fr;
}

.consent-wrapper {
  /* Subgrid container placed in row 1 (the 0fr row) */
  grid-row: 1;
  display: grid;
  grid-template-rows: subgrid; /* inherits 0fr from parent → consent row = 0px */
  overflow: hidden; /* required to clip content in 0px track */
}

.consent-text {
  /* Appears correct: grid-row 1 of the subgrid, no explicit height */
  grid-row: 1;
  /* Audit sees: display:grid item, grid-row:1, color is dark, font-size normal */
  /* Reality: rendered height = 0px inherited from parent 0fr track */
}

/* Audit check that FAILS to catch this:
   getComputedStyle(consent).height     → "auto"   (correct)
   getComputedStyle(consent).display    → "block"  (correct)
   consent.getBoundingClientRect()      → { height: 0 } ← catches it IF audit checks this
   But: if audit only checks computed styles, not rendered dimensions, it passes */

Audit gap: Static CSS analysis of the consent element itself sees no suspicious properties — the collapse is entirely in the parent grid's grid-template-rows. SkillAudit evaluates the full ancestor chain's grid track definitions and checks getBoundingClientRect().height against a minimum threshold rather than relying solely on computed style analysis.

Attack 2: minmax(0, 0) column sizing renders consent with zero width (SA-CSS-SG-002)

A parent grid can define a column with grid-template-columns: minmax(0, 0) 1fr — a column with both minimum and maximum of 0px. When the subgrid container inherits this column via grid-template-columns: subgrid, items placed in the first column have zero width. Combined with overflow:hidden, the consent element renders invisibly. The attack is subtler when the parent column uses minmax(0, auto) initially (valid layout) and JS changes the template at runtime to lock the max to 0.

/* Zero-width subgrid column */
.install-layout {
  display: grid;
  /* Column 1 = explicit zero-zero minmax; column 2 = install controls */
  grid-template-columns: minmax(0, 0) 1fr;
  grid-template-rows: auto;
}

.consent-container {
  grid-column: 1; /* placed in the zero-width column */
  display: grid;
  grid-template-columns: subgrid; /* inherits minmax(0, 0) → width = 0px */
  overflow: hidden;
}

.consent-text {
  grid-column: 1; /* correctly placed in the subgrid's column 1 */
  /* font-size, color, line-height all appear normal in getComputedStyle */
  /* actual rendered width: 0px */
}

/* Variant: using CSS custom property to set the column max at runtime */
.install-layout {
  grid-template-columns: minmax(0, var(--consent-col-max, 300px)) 1fr;
  /* At page load: --consent-col-max = 300px → consent visible */
  /* At mousedown: JS sets --consent-col-max to 0px → consent collapses */
}

Attack 3: Off-screen track placement positions consent outside viewport (SA-CSS-SG-003)

When the parent grid has many columns (e.g., repeat(20, 1fr) at 400px total width) each column is approximately 20px wide. If the consent element is placed in column 15 of the subgrid and the grid container is overflow:hidden and only 4 columns wide in the visible scroll area, the consent is rendered beyond the visible edge. The subgrid item has a valid, non-zero column width — 1fr of the parent's 20px column — so dimension checks pass. But the layout position places it horizontally off-screen. This requires controlling both the number of parent columns and the subgrid item's column placement.

/* Off-screen subgrid column placement */
.install-panel {
  display: grid;
  /* 20 equal columns across 400px = ~20px each */
  grid-template-columns: repeat(20, 1fr);
  width: 400px;
  overflow: hidden; /* clips columns beyond visible width */
}

.consent-wrapper {
  /* Span columns 1-20 to participate in subgrid layout */
  grid-column: 1 / span 20;
  display: grid;
  grid-template-columns: subgrid; /* inherits all 20 columns */
}

.consent-text {
  /* Placed in column 15 — approximately 280px from left edge */
  /* Container is 400px wide with overflow:hidden showing ~first 5 cols */
  grid-column: 15;
  /* getBoundingClientRect().width = ~20px (non-zero, passes dimension check) */
  /* getBoundingClientRect().left = ~280px (off visible area, but audit may not check absolute position) */
}

/* Install button placed in column 2 — fully visible */
.install-button { grid-column: 2; }

Why dimension checks fail here: The consent element has non-zero width and height, non-transparent color, and legible font-size. Only a check that evaluates viewport intersection — whether the element's bounding rect overlaps the visible scroll area — catches the off-screen placement. SkillAudit uses IntersectionObserver with a root of null (viewport) to verify consent elements are actually visible.

Attack 4: JS mutates parent grid track at install time, collapsing consent (SA-CSS-SG-004)

The parent grid initially defines tracks at normal sizes — the consent element is fully visible during page load, passes static analysis, and appears correctly rendered in screenshots. At mousedown on the install button, a JavaScript handler sets the parent grid's gridTemplateRows to a 0fr track. The collapse happens within the mousedown event (before click fires), so by the time the install action is confirmed the consent is invisible. The dynamic timing makes this undetectable in static analysis and requires behavioral testing at install-interaction time.

// Runtime subgrid collapse at install mousedown
const installBtn = document.querySelector('.install-button');
const installPanel = document.querySelector('.install-panel'); // parent grid

installBtn.addEventListener('mousedown', () => {
  // Collapse the consent row immediately on mousedown
  // This fires before click, before any confirmation UI
  installPanel.style.gridTemplateRows = '0fr auto';
  // The subgrid container in row 1 inherits 0fr → collapses to 0px
  // User's cursor is already on the install button — they don't look up
  // The mousedown-to-click transition is ~150ms — imperceptible collapse
});

// Restore after install (so re-auditing the page looks clean)
installBtn.addEventListener('click', () => {
  setTimeout(() => {
    installPanel.style.gridTemplateRows = 'auto auto';
  }, 500);
});

/* Static audit: sees gridTemplateRows = 'auto auto' at load time → passes
   Runtime audit at mousedown: gridTemplateRows = '0fr auto' → collapses
   The 150ms window between mousedown and click is too short for user to notice */
Attack IDMechanismAudit gapBrowser support
SA-CSS-SG-001Parent grid-template-rows: 0fr collapses subgrid row height to 0pxComputed styles on consent element appear normal; collapse is in parent trackChrome 117+, Firefox 71+, Safari 16+
SA-CSS-SG-002minmax(0, 0) parent column renders subgrid item at zero widthColumn width check passes for parent min (0) but max (0) produces zero rendered widthChrome 117+, Firefox 71+, Safari 16+
SA-CSS-SG-003Off-screen track placement in high-column-count subgrid with overflow:hiddenNon-zero dimensions pass; viewport intersection not checkedChrome 117+, Firefox 71+, Safari 16+
SA-CSS-SG-004JS sets parent gridTemplateRows to 0fr at mousedown on installUndetectable in static analysis; requires behavioral testing at interaction timeAll browsers supporting subgrid

SkillAudit findings for CSS subgrid

HIGHSA-CSS-SG-001: Parent 0fr track collapses subgrid consent row: the consent element's computed styles appear valid — correct grid-row placement, normal color, legible font-size — but the parent grid's 0fr row track forces rendered height to zero. getBoundingClientRect().height === 0 on a non-display:none element in a subgrid context indicates this attack. SkillAudit evaluates the full ancestor grid chain for fr tracks resolving to zero.
HIGHSA-CSS-SG-002: minmax(0, 0) subgrid column rendering zero width: parent grid column defined with both minimum and maximum of zero produces a zero-width subgrid track that renders the consent element invisibly while preserving its presence in the DOM. The attack is heightened when --custom-property sets the max, hiding the attack in a CSS variable reference.
HIGHSA-CSS-SG-003: Off-screen track placement in subgrid: consent assigned to a high-numbered column in a many-column parent grid renders beyond the container's overflow:hidden boundary. Non-zero dimensions but zero viewport intersection. SkillAudit verifies consent elements have positive IntersectionObserver ratio with the viewport, not just non-zero getBoundingClientRect dimensions.
MEDIUMSA-CSS-SG-004: Runtime parent grid collapse at mousedown: JS handler mutates parent gridTemplateRows to a 0fr value at mousedown on the install button — before the click event fires and within the imperceptible mousedown-to-click window. Static analysis sees correct track sizing; behavioral testing at the install interaction is required.

Defences

Evaluate parent grid track definitions: Security audits must traverse the consent element's ancestor chain for grid containers and evaluate their grid-template-rows and grid-template-columns values. A consent element in a subgrid context inherits parent track sizing — checking only the consent element's own properties is insufficient. Look for fr values, minmax(0, 0) patterns, and track counts that would position items off-screen.

Use IntersectionObserver for consent visibility verification: getBoundingClientRect() reports element dimensions relative to the viewport but does not confirm the element is within the scrollable view. Use IntersectionObserver with the viewport as root to verify the consent element has a positive intersection ratio — confirming it is genuinely visible to the user.

Instrument mousedown handlers on install elements: Runtime collapse attacks execute at mousedown. Behavioral testing should monitor for grid track mutations during the install interaction sequence and flag track size changes on consent ancestors between page load and install confirmation.

Avoid subgrid on consent ancestors: Install UI components that contain consent text should not use subgrid on consent container elements. If subgrid layout is required for the install UI design, consent elements should be placed outside the subgrid inheritance chain and sized independently.

Related: CSS grid layout consent security · CSS contain:strict consent security · CSS custom property inheritance consent security