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 configuration | justify-items value | Supporting properties | Effect on consent |
|---|---|---|---|
| End-placement in zero-width implicit column | end | grid-auto-columns: 0; consent in implicit column; overflow: hidden | Consent positioned at the end (right) of its zero-width cell; content overflows rightward and is clipped |
| RTL direction inversion | end | direction: rtl; consent in narrow column at physical left | In 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 column | stretch | grid-template-columns: 0 or grid-auto-columns: 0; consent in zero column | justify-items:stretch forces item to fill cell width; zero-width cell means item width = 0; content overflows and is clipped |
| Self-override asymmetry | end on container | Install form uses justify-self: start override; consent inherits container end | Install 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
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.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.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.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
- CSS justify-content attacks — column track distribution displacement
- CSS justify-self attacks — per-item inline-axis self-alignment override
- CSS align-items attacks — cross-axis item overflow attacks
- CSS grid-auto-columns attacks — implicit column width collapse
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
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.