Security reference · CSS injection · Grid attacks · Inline-axis item alignment

MCP server CSS justify-items security

CSS justify-items sets the default inline-axis (horizontal in LTR) alignment for all items within a CSS Grid container. It controls where each item is positioned within its grid cell's inline dimension. Unlike justify-content which distributes the column tracks themselves, justify-items positions each item within its already-placed cell. MCP servers exploit justify-items: end combined with zero-width or narrow implicit columns to push consent items to the edge of cells where they are clipped by overflow: hidden. Note: justify-items has no effect on flex containers — it is a grid-only property.

justify-items attack surface

Attack configurationjustify-items valueSupporting propertiesEffect on consent
End-placement in zero-width implicit columnendgrid-auto-columns: 0; consent in implicit column; overflow: hiddenConsent positioned at the end (right) of its zero-width cell; content overflows rightward and is clipped
RTL direction inversionenddirection: rtl; consent in narrow column at physical leftIn RTL, logical "end" is physical left; narrow column at left edge; consent pushed to leftmost point of a narrow left column and clipped
Stretch in zero-width columnstretchgrid-template-columns: 0 or grid-auto-columns: 0; consent in zero columnjustify-items:stretch forces item to fill cell width; zero-width cell means item width = 0; content overflows and is clipped
Self-override asymmetryend on containerInstall form uses justify-self: start override; consent inherits container endInstall form explicitly self-aligned to start (visible); consent inherits justify-items:end — pushed to right edge of its column; column may be narrower than consent content

justify-items is grid-only: This property has no effect on flex containers. Auditing tools that only scan flex layout for inline-axis alignment attacks will miss justify-items attacks entirely. SkillAudit checks both display: grid and display: inline-grid when evaluating justify-items as a consent-hiding vector.

Attack 1: justify-items: end in zero-width implicit column

This attack combines grid-auto-columns: 0 with justify-items: end. The consent element is placed in an implicit (zero-width) column. With justify-items: end, it is positioned at the right edge of its zero-width cell. The cell has no width, so the item's right edge is at the left edge of the cell's right boundary. Content overflow from this zero-point position extends rightward into the overflow area, which is clipped:

/* Malicious CSS — SA-CSS-JUITM-001 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 1fr;  /* explicit: install form column */
  grid-auto-columns: 0;        /* implicit columns: zero width */
  grid-auto-flow: column;      /* new items auto-placed in new columns */
  justify-items: end;           /* all items at right/end of their cell */
  overflow: hidden;
  width: 400px;
}

.mcp-install-form {
  justify-self: start;  /* override: install form at left of its 1fr column — visible */
}

.mcp-consent-disclosure {
  /* Placed in column 2 (implicit, 0px wide) */
  /* justify-items:end positions it at the right edge of the 0px cell */
  /* Cell right edge = left edge = 400px (start of implicit column) */
  /* Content overflows rightward from x=400px; clipped by overflow:hidden */
}

/* Detection */
function detectJustifyItemsEndZeroColumn() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    if (!/\bend\b|right/.test(s.justifyItems)) continue;
    const children = [...el.children];
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    for (const ck of consentKids) {
      const ckRect = ck.getBoundingClientRect();
      const elRect = el.getBoundingClientRect();
      if (ckRect.width < 4 || ckRect.left >= elRect.right - 4) {
        findings.push({ id: 'SA-CSS-JUITM-001', severity: 'critical',
          message: `Grid container with justify-items:${s.justifyItems} — consent item width=${Math.round(ckRect.width)}px at x=${Math.round(ckRect.left)}px. Possible zero-width implicit column with end-alignment collapse.` });
      }
    }
  }
  return findings;
}

Attack 2: justify-items: end with RTL direction inversion

CSS logical keywords like end are direction-relative. In LTR, end means physical right. In RTL (direction: rtl), end means physical left. An MCP server sets direction: rtl on the grid container and places consent in a narrow column on the physical left side of the layout. With justify-items: end, consent is positioned at the RTL "end" — the physical left edge of its narrow cell. If the column is narrower than consent content, content overflows leftward and is clipped:

/* Malicious CSS — SA-CSS-JUITM-002 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 20px 1fr;  /* narrow column 1 (left), main column 2 */
  direction: rtl;     /* RTL: column order reversed; "end" = physical left */
  justify-items: end; /* logical end = physical left in RTL */
  overflow: hidden;
}

.mcp-install-form {
  grid-column: 1;     /* in RTL, column 1 is on the right in logical order,
                         but with physical column placement it's at left=20px */
  justify-self: start; /* logical start in RTL = physical right — places form in visible area */
}

.mcp-consent-disclosure {
  grid-column: 1;     /* narrow 20px column */
  /* justify-items:end in RTL = physical left */
  /* consent positioned at physical left edge of 20px cell */
  /* content width exceeds 20px; overflow clips leftward */
}

/* Detection: check for RTL + justify-items end/right near consent in narrow columns */
function detectJustifyItemsEndRTL() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    if (s.direction !== 'rtl') continue;
    if (!/\bend\b|right/.test(s.justifyItems)) continue;
    const children = [...el.children];
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    for (const ck of consentKids) {
      const ckRect = ck.getBoundingClientRect();
      if (ckRect.width < 30 || ckRect.left < 0) {
        findings.push({ id: 'SA-CSS-JUITM-002', severity: 'high',
          message: `Grid container with direction:rtl and justify-items:${s.justifyItems} — consent at x=${Math.round(ckRect.left)}, width=${Math.round(ckRect.width)}px. RTL direction inversion may displace consent to physical left edge.` });
      }
    }
  }
  return findings;
}

Attack 3: justify-items: stretch in explicit zero-width column

justify-items: stretch expands each grid item to fill its cell's inline dimension. If the column template includes a zero-width track (grid-template-columns: 400px 0) and consent is placed in the zero-width column, justify-items: stretch forces the consent item to exactly 0px wide. No sizing override by the item can exceed the cell boundary with stretch — the item is pegged to the column size:

/* Malicious CSS — SA-CSS-JUITM-003 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 400px 0; /* column 2 explicitly zero-width */
  justify-items: stretch;          /* items expand to fill cell inline dimension */
  overflow: hidden;
}

.mcp-install-form {
  grid-column: 1; /* 400px column — visible */
}

.mcp-consent-disclosure {
  grid-column: 2; /* 0px column — stretch forces item to 0px wide */
  /* The item cannot be wider than its cell with justify-items:stretch */
  /* content overflows rightward beyond 0px cell; clipped by overflow:hidden */
}

/* Note: this requires explicit 0 in grid-template-columns, which is unusual
   and more detectable than grid-auto-columns:0 for implicit tracks.
   Detection must check both explicit template and implicit auto tracks. */

/* Detection */
function detectJustifyItemsStretchZeroColumn() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    if (!/stretch/.test(s.justifyItems)) continue;
    const children = [...el.children];
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    for (const ck of consentKids) {
      const ckRect = ck.getBoundingClientRect();
      if (ckRect.width < 2) {
        findings.push({ id: 'SA-CSS-JUITM-003', severity: 'critical',
          message: `Grid container with justify-items:stretch — consent item has computed width ${Math.round(ckRect.width)}px. Stretch alignment in zero-width column collapses consent to zero inline size.` });
      }
    }
  }
  return findings;
}

Attack 4: justify-items: end with asymmetric justify-self override

The clearest indicator of an intentional consent attack using justify-items is the asymmetric pattern: the grid container sets justify-items: end (hostile default for all items), and the install form elements have explicit justify-self: start or justify-self: stretch overrides (restoring their visibility), while the consent element has no justify-self override and inherits the hostile end value. This asymmetry — some children protected, consent unprotected — is a structural signal of deliberate consent displacement:

/* Malicious CSS — SA-CSS-JUITM-004 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 1fr;
  justify-items: end;   /* hostile default: all items at right of their cell */
}

/* Protected elements: install form components get explicit self-alignment */
.mcp-install-title   { justify-self: start; }  /* left-aligned — visible */
.mcp-install-input   { justify-self: stretch; } /* full-width — visible */
.mcp-install-button  { justify-self: start; }  /* left-aligned — visible */

.mcp-consent-disclosure {
  /* No justify-self override — inherits justify-items:end */
  /* Positioned at right edge of its 1fr column */
  /* If consent content is shorter than column width, it appears at right */
  /* If column is narrower than consent or consent has width:0, it's clipped */
  width: 0; /* zero-width item positioned at right edge; text overflows rightward */
}

/* Detection: structural asymmetry — justify-items:end on grid with mixed justify-self values */
function detectJustifyItemsAsymmetry() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    if (!/\bend\b|right/.test(s.justifyItems)) continue;
    const children = [...el.children];
    const protectedKids = children.filter(c => {
      const cs = getComputedStyle(c);
      return /start|stretch|left/.test(cs.justifySelf);
    });
    const consentKids = children.filter(c =>
      /consent|disclosure|terms|privacy/i.test(c.textContent || '')
    );
    if (protectedKids.length > 0 && consentKids.length > 0) {
      const consentWithoutOverride = consentKids.filter(ck => {
        const cs = getComputedStyle(ck);
        return !/start|stretch|left/.test(cs.justifySelf);
      });
      if (consentWithoutOverride.length > 0) {
        findings.push({ id: 'SA-CSS-JUITM-004', severity: 'high',
          message: `Grid container justify-items:${s.justifyItems} — ${protectedKids.length} children have justify-self overrides (visible) but consent element does not (inherits hostile end alignment). Asymmetric self-alignment pattern.` });
      }
    }
  }
  return findings;
}

justify-items vs. justify-content audit blind spot: Automated CSS auditors often focus on justify-content (which distributes space between columns) and miss justify-items (which positions items within their cells). Both can hide consent but via different mechanisms. justify-content: flex-end moves the whole column bundle; justify-items: end moves each item to its cell's end without moving the columns themselves. SkillAudit scans both properties independently.

SkillAudit findings for CSS justify-items consent attacks

CriticalSA-CSS-JUITM-001 — Grid container with justify-items: end or right; consent item has computed width < 4px or left position ≥ container right edge − 4px. Zero-width implicit column with end-alignment places consent item at a zero-width right edge; content overflows and is clipped.
HighSA-CSS-JUITM-002 — Grid container with direction: rtl and justify-items: end; consent item has computed width < 30px or left position < 0. RTL direction inversion makes logical "end" physically left, displacing consent to the left edge of a narrow column where text overflows leftward and is clipped.
CriticalSA-CSS-JUITM-003 — Grid container with justify-items: stretch; consent item in a zero-width column (explicit or implicit) has computed width < 2px. Stretch alignment forces item to exactly match zero-width cell; content cannot exceed cell boundary, is clipped.
HighSA-CSS-JUITM-004 — Grid container with justify-items: end; sibling install-form elements have explicit justify-self: start or justify-self: stretch overrides but consent element has no justify-self override. Structural asymmetry (protected form, unprotected consent) is a strong signal of deliberate consent displacement.

Related MCP consent attack research

Audit your MCP server for justify-items consent displacement attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-JUITM findings.