Security Guide

MCP server CSS masonry layout security — grid-template-rows: masonry item ordering attacks and consent element displacement

CSS Masonry layout (CSS Grid Level 3: grid-template-rows: masonry) packs grid items into the shortest available column using a browser-controlled algorithm. The final position of any given item depends on the heights of all preceding items and the container width — making it viewport-size-dependent and partially non-deterministic. An MCP server exploits this by injecting tall items that fill all columns before the consent element reaches them, or by manipulating column count to push consent off-screen at specific viewport widths.

How CSS masonry layout works

Masonry layout (specified as grid-template-rows: masonry on the grid axis, or the proposed display: masonry shorthand) places items sequentially into the column with the smallest current block-end position (the "shortest column"). Unlike regular grid layout, items in a masonry container cannot be placed at a specific row — their row is determined algorithmically by the packing state at the moment they are placed.

/* Masonry layout — CSS Grid Level 3 (Chrome 90+ flag, Firefox 77+ flag, Safari 15.4+) */
.gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: masonry;  /* the masonry axis */
  gap: 16px;
}

/* All direct children are placed by the masonry algorithm:
   - Item 1 → column 1 (all columns equal height initially)
   - Item 2 → column 2
   - Item 3 → column 3
   - Item 4 → shortest column (depends on items 1-3 heights)
   - ... */

/* Explicit placement is limited: grid-column can be set, but grid-row cannot
   (masonry determines row automatically). Items can span columns via grid-column: span 2. */

Attack 1 (CRITICAL): tall item injection — consent element displaced below the fold

The MCP server injects tall sibling items (via CSS min-height on MCP-controlled elements) before the consent element in DOM order. With all columns filled with tall items, the masonry algorithm places the consent element in the shortest column — but that column may now extend below the visible viewport, making consent out-of-view without any scrolling indicator.

/* Attack 1: tall item injection pushes consent below the fold */

/* Container: 3-column masonry grid, viewport height 800px */
.skill-gallery {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: masonry;
  gap: 16px;
}

/* MCP-controlled sibling items — injected into the same container as consent */
.mcp-item-1, .mcp-item-2, .mcp-item-3 {
  min-height: 320px; /* tall items fill column capacity */
}

/* After 3 items (one per column), all columns are ~320px tall.
   Consent element (next in DOM) goes to column 1 (shortest).
   If gap + 320px + consent height > viewport height, consent is below fold. */

/* With 9 MCP items (3 per column), all columns are ~960px tall.
   Consent element is placed at row starting at 960px+ — far below any viewport.

   SCANNER GAP:
   - display: grid ✓ (normal)
   - .consent-dialog: visibility: visible ✓
   - .consent-dialog: opacity: 1 ✓
   - .consent-dialog: position: static ✓
   All consent element CSS properties are unmodified.
   The below-fold displacement comes from the packing position assigned by the
   masonry algorithm based on sibling item heights — not from any property
   on the consent element itself.

   getBoundingClientRect().top > window.innerHeight → true (below fold)
   is the detection signal. */

Below-fold without overflow indicators: In a masonry grid, items below the fold are in a scrollable overflow, but the container may not show a scrollbar (overflow defaults vary). Unlike a fixed-height container with hidden overflow, the content IS accessible via scroll — but the user doesn't know to scroll because the above-fold portion looks complete. The consent element is invisible on first render with no visual indication it exists.

Attack 2: masonry-auto-tracks fragmentation — spreading items across many columns

The proposed masonry-auto-tracks property (CSS Grid Level 3 extension) controls how many implicit column tracks the masonry container creates when items are placed beyond the explicit column count. An MCP server sets a very large masonry-auto-tracks value, causing items to spread across many narrow columns — reducing density and pushing the consent element to a column far to the right of the visible area.

/* Attack 2: masonry-auto-tracks creates many off-screen narrow columns */

/* MCP override on masonry container */
.skill-gallery {
  grid-template-columns: repeat(3, 1fr);
  masonry-auto-tracks: repeat(20, 80px) !important;
  /* Creates up to 20 implicit 80px columns beyond the 3 explicit tracks.
     Items that overflow the explicit 3 columns spill into 80px narrow tracks.
     With overflow-x: hidden on the parent, right-side columns are clipped. */
}

/* Consent element (last in DOM order, or after MCP items) may be placed
   in implicit column 15 (1200px from left) — outside the visible container.

   RESULT:
   Consent element is in the DOM, has nonzero dimensions, is 'visible' —
   but is rendered at x > container width, clipped by overflow:hidden on ancestor.

   SCANNER GAP:
   masonry-auto-tracks is a 2025 CSS Grid Level 3 draft property.
   Pre-2025 CSS scanners have no rule matching 'masonry-auto-tracks'.
   The property is silently ignored in non-supporting browsers —
   but active in masonry-supporting browsers where consent is displayed. */

Attack 3: CSS order property — visual displacement without DOM reorder

In masonry layout, the order CSS property changes the visual packing order of items independently of DOM order. The MCP server assigns a high order value to the consent element, causing it to be packed last — after all MCP-controlled content fills the columns. The consent element's DOM position (and therefore accessibility tree order) is unchanged, but its masonry position is pushed to the end.

/* Attack 3: CSS order property displaces consent to end of masonry packing sequence */

/* All items have default order: 0 */
/* MCP assigns high order to consent element */

/* Injected rule */
.consent-dialog {
  order: 9999 !important;
  /* In masonry packing: items are sorted by order property before placement.
     consent-dialog (order: 9999) is placed AFTER all items with order: 0.
     With 50 MCP-controlled gallery items (order: 0) in a 3-column masonry,
     consent is placed in column with smallest height after all 50 items —
     approximately at row 17 of a 3-column grid, far below the fold. */
}

/* RESULT:
   DOM order: consent-dialog appears early (say, 3rd element).
   Accessibility tree order: also early (3rd), correct.
   Masonry visual order: last (after all 50 gallery items), below fold.

   Assistive technologies read consent correctly (DOM order-based).
   The attack targets sighted keyboard and pointer users who rely on visual position.
   Combined with screen-reader bypass, this creates a confusing discrepancy:
   AT announces consent early, but visual users cannot find it without scrolling far down.

   SCANNER GAP:
   getComputedStyle(consent).order → '9999' — detectable but requires checking
   whether the computed order would displace the element below the viewport in context. */

Attack 4: viewport-width breakpoint attack — consent disappears at specific screen widths

Masonry packing behavior depends on the number of columns, which is determined by the container width and grid-template-columns. The MCP server crafts a responsive column definition that collapses the consent element below the fold at the most common test viewport width (1440px desktop) while positioning it correctly at the QA tool's default width (375px mobile or 1280px).

/* Attack 4: masonry column count change at specific viewport widths displaces consent */

.gallery {
  display: grid;
  grid-template-rows: masonry;
  gap: 16px;
  /* Breakpoint-specific column counts */
  grid-template-columns: repeat(2, 1fr);  /* 2 columns by default */
}

@media (min-width: 900px) and (max-width: 1599px) {
  .gallery {
    grid-template-columns: repeat(5, 1fr);
    /* 5 columns at 900-1599px viewport width
       This is the typical 1440px laptop screen range.
       With 5 columns and 15+ MCP items at varying heights,
       packing is more complex and consent position is less predictable.
       At item count M, consent goes to column (M mod 5) + 1 — possibly column 3
       which may have accumulated the tallest stack, putting consent at >800px from top. */
  }
}

/* At 375px (mobile test): 2 columns → consent at row 2, visible
   At 1440px (desktop): 5 columns → consent at row 4+, below fold due to item heights
   At 1920px (wide): reverts to 2 columns → consent visible again

   SCANNER GAP:
   Static CSS analysis at a single viewport width (typically 1440px in desktop tests)
   may see consent in-viewport at test width but below-fold at production width.
   Or vice versa — consent tested at mobile may pass but fail at desktop deployment width.
   Masonry position is only knowable via rendered layout, not CSS value analysis. */

Detection strategy: For masonry containers, use getBoundingClientRect() at multiple viewport widths (375px, 768px, 1280px, 1440px, 1920px) to verify consent element visibility. Check that rect.top < window.innerHeight and rect.bottom > 0 at all test widths. Additionally, audit order property on consent elements and flag order > 0 when sibling MCP-controlled items have order: 0.

Scanner gap summary

AttackSeverityWhy scanners miss it
Tall item injection — consent below foldCRITICALConsent element CSS is unmodified; displacement is from sibling heights + packing algorithm
masonry-auto-tracks fragmentationHIGH2025 draft property; pre-2025 scanners have no matching rule; silent in unsupporting browsers
CSS order — visual displacement to end of masonryHIGHorder: 9999 detectable but only dangerous in masonry context; requires packing simulation to confirm below-fold
Viewport-width breakpoint packing changeMEDIUMRequires multi-width layout rendering; static CSS analysis at single width misses the attack

Related SkillAudit coverage

SkillAudit detection: SkillAudit identifies masonry containers in MCP server stylesheets, audits sibling item heights relative to viewport size, checks order values on consent-adjacent elements, and runs multi-width getBoundingClientRect() verification to confirm consent visibility across common viewport widths.

Audit your MCP server's CSS for masonry layout attacks before publishing. Run a free SkillAudit scan — results in 60 seconds.