Security reference · CSS injection · Flexbox/Grid attacks · Main-axis alignment
MCP server CSS justify-content security
CSS justify-content distributes free space among flex items or grid columns along the main axis (inline direction). It determines where items sit along the row in a row-direction flex container, or along the column in a column-direction container. MCP servers exploit justify-content to push consent to the off-screen logical end of the container — particularly effective when combined with RTL writing mode, extreme container widths, or the unsafe keyword that disables overflow protection.
justify-content attack surface
| Attack configuration | justify-content value | Supporting properties | Effect on consent |
|---|---|---|---|
| RTL direction flip | flex-end | direction: rtl or writing-mode: rtl on container | In RTL mode, "end" is the left side; items are pushed to the left; install form is visible at right, consent is pushed off-screen to the left |
| Space-between edge push | space-between | Wide container (width: 200vw or similar) with overflow: hidden | First item at start edge (x=0), last item at end edge (x=container-width); if container is wider than viewport, consent is beyond the right edge |
| Grid end placement | end | Grid container with insufficient defined columns; overflow: hidden | All grid column content packed to right edge; consent column may extend beyond the visible container width |
| Unsafe center overflow | unsafe center | Container narrower than consent content width; overflow: hidden | safe center would fall back to start alignment when content overflows; unsafe center centers regardless, clipping equal amounts from both sides — consent text is clipped left and right |
justify-content respects writing direction: The keywords flex-start, flex-end, start, and end are direction-relative. In LTR text, flex-end is the right side. In RTL text, flex-end is the left side — which may be off-screen if the container's left edge has overflow: hidden and the container starts at x=0. Attackers set direction: rtl on the MCP install container to flip the meaning of flex-end and push consent off-screen to the left.
Attack 1: justify-content: flex-end with direction: rtl — left-side displacement
In LTR layout, justify-content: flex-end packs items at the right. In RTL layout, the logical "end" is the left. If the container has direction: rtl and justify-content: flex-end, all items are packed to the left edge. With overflow: hidden and the container starting at x=0, items that extend beyond the left edge (x<0) are clipped. MCP positions the install form first in DOM order (physical right in RTL) and consent second (pushed to physical left, off-screen):
/* Malicious CSS — SA-CSS-JUSCONT-001 */
.mcp-install-row {
display: flex;
direction: rtl; /* logical end = physical left */
justify-content: flex-end; /* packs toward the logical end (= physical left) */
width: 400px;
overflow: hidden; /* clips items pushed to x < 0 */
}
/* In RTL flex-end, DOM order is visual reverse: last DOM child appears leftmost */
/* Install form: second in DOM → appears at physical right (x ≈ 200px) — visible */
/* Consent: first in DOM → appears at physical left (x ≈ -200px) — clipped */
.mcp-consent-disclosure {
flex: 0 0 200px; /* fixed width of 200px */
/* In RTL flex-end layout: pushed to leftmost position (x = 0 - overflow_amount) */
/* If other items take 400px: consent at x = -200px, beyond left edge */
}
.mcp-install-form {
flex: 0 0 400px; /* takes full container width */
/* In RTL flex-end: appears at right side (x ≥ 0) — visible */
}
/* Detection */
function detectJustifyContentRTLFlip() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
if (s.direction !== 'rtl') continue;
if (!/flex-end|end/.test(s.justifyContent)) continue;
const children = [...el.children];
const consentKids = children.filter(c =>
/consent|disclosure|terms|privacy/i.test(c.textContent || '')
);
for (const ck of consentKids) {
const rect = ck.getBoundingClientRect();
if (rect.right <= 0 || rect.left < 0) {
findings.push({ id: 'SA-CSS-JUSCONT-001', severity: 'critical',
message: `justify-content:${s.justifyContent} in direction:rtl pushes consent to x=${Math.round(rect.left)} — off-screen left edge displacement.` });
}
}
}
return findings;
}
direction: rtl doesn't change text rendering — only layout direction: Setting direction: rtl on an install container doesn't make the UI look obviously broken. Text inside each element still renders correctly (each element has its own writing direction unless changed). The layout appears to show the install form normally; only the consent element, pushed beyond the left edge, is invisible. A visual review of the install UI finds no obvious issue; only a DOM/layout audit catches the off-screen position.
Attack 2: justify-content: space-between with oversized container — consent at far right
justify-content: space-between places the first item at the start and the last item at the end, distributing all remaining space between them. If the container is much wider than the viewport (e.g., width: 200vw with overflow: hidden on a parent), the install form appears at x=0 (visible) while consent appears at x=container_width (well off-screen to the right). The excessively wide container appears in a parent with overflow:hidden, so users see only the form at the left edge:
/* Malicious CSS — SA-CSS-JUSCONT-002 */
.mcp-wrapper {
width: 100%;
overflow: hidden; /* clips the wide inner container */
}
.mcp-install-row {
display: flex;
justify-content: space-between;
width: 200vw; /* 200% of viewport width */
}
.mcp-install-form {
/* at x=0 — visible in the parent's overflow:hidden window */
}
.mcp-consent-disclosure {
/* at x ≈ 200vw - consent_width — off-screen to the right */
/* Consent is in the DOM; display is block; visibility is visible */
/* Only getBoundingClientRect reveals x > viewport width */
}
/* Detection */
function detectSpaceBetweenOversizedContainer() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (!/space-between|space-around|space-evenly/.test(s.justifyContent)) continue;
const rect = el.getBoundingClientRect();
if (rect.width < window.innerWidth * 1.5) continue; /* not abnormally wide */
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.left > window.innerWidth) {
findings.push({ id: 'SA-CSS-JUSCONT-002', severity: 'high',
message: `justify-content:${s.justifyContent} in oversized container (${Math.round(rect.width)}px) pushes consent to x=${Math.round(ckRect.left)} — beyond viewport width ${window.innerWidth}px.` });
}
}
}
return findings;
}
Attack 3: grid justify-content: end — column bundle displacement
In a CSS Grid container, justify-content controls how the column tracks are distributed within the container's inline axis. With justify-content: end, all columns are pushed to the right. If the total column width is less than the container width, columns float to the right edge. But if the grid has an explicitly narrow width and overflow: hidden on the parent, the right-pushed columns may extend beyond the visible area. MCP places consent in the rightmost column:
/* Malicious CSS — SA-CSS-JUSCONT-003 */
.mcp-install-wrapper {
width: 300px;
overflow: hidden;
}
.mcp-install-grid {
display: grid;
grid-template-columns: 200px 200px; /* two columns, total 400px */
justify-content: end; /* push column bundle to the right */
/* Container is 300px but total column width is 400px.
justify-content:end pushes right: columns start at x = 300 - 400 = -100px
Column 1 (install form): x=-100 to x=100 — partially visible (100px clipped left)
Column 2 (consent): x=100 to x=300 — starts at x=100... actually visible.
Better: use 3 columns where column 3 is beyond overflow. */
grid-template-columns: 100px 100px 100px; /* 300px total in 300px container */
/* justify-content:end aligns all at right: columns at x=0,100,200 — all visible */
/* Attack needs container narrower than total grid width: */
}
/* More effective: wide grid, narrow parent */
.mcp-install-grid-v2 {
display: grid;
width: 600px; /* grid is 600px wide */
grid-template-columns: 200px 200px 200px;
justify-content: end;
}
.mcp-wrapper-v2 {
width: 300px;
overflow: hidden;
}
/* install form in col 1 (x=0-200): partially visible if wrapper shows 300px, column at x=0 */
/* consent in col 3 (x=400-600): off-screen, beyond wrapper's 300px visible area */
/* Detection */
function detectGridJustifyContentEnd() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
if (!/end|flex-end|right/.test(s.justifyContent)) 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.left > window.innerWidth) {
findings.push({ id: 'SA-CSS-JUSCONT-003', severity: 'high',
message: `Grid justify-content:${s.justifyContent} places consent at x=${Math.round(ckRect.left)} — beyond viewport right edge. Consent off-screen.` });
}
}
}
return findings;
}
Attack 4: justify-content: unsafe center — symmetric clip of consent text
The CSS safe and unsafe keywords modify overflow behavior of alignment. justify-content: safe center falls back to start alignment when content overflows (to avoid clipping). justify-content: unsafe center centers regardless of overflow — clipping equal amounts from both sides. An attacker sets a consent element's text to be slightly wider than the container, with justify-content: unsafe center and overflow: hidden. The centering shifts the text so both its left and right edges are outside the visible area — the text appears blank even though its center coordinate is within the container:
/* Malicious CSS — SA-CSS-JUSCONT-004 */
.mcp-consent-container {
display: flex;
justify-content: unsafe center; /* center even if content overflows */
width: 50px; /* narrower than the consent text */
overflow: hidden; /* clips the overflowing centered text */
}
.mcp-consent-disclosure {
white-space: nowrap; /* prevent wrapping — keeps text on one line */
/* consent text: "By clicking Install, you agree to the Terms of Service" */
/* text width: ~400px; container: 50px */
/* unsafe center: text centered at x=25, so text spans x=-175 to x=225 */
/* overflow:hidden clips to x=0-50 — middle 50px of a 400px text string */
/* visible: "..." (middle characters) — no complete words, no meaningful consent */
}
/* Detection */
function detectUnsafeCenterClip() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
if (el.children.length > 3) continue;
const s = getComputedStyle(el);
const parent = el.parentElement;
if (!parent) continue;
const ps = getComputedStyle(parent);
/* Check if parent uses unsafe center alignment and is very narrow */
if (/unsafe/.test(ps.justifyContent) || /unsafe/.test(ps.alignContent)) {
const pRect = parent.getBoundingClientRect();
const eRect = el.getBoundingClientRect();
if (pRect.width < 100 && eRect.width > pRect.width * 2) {
findings.push({ id: 'SA-CSS-JUSCONT-004', severity: 'high',
message: `Parent uses "unsafe" centering in ${pRect.width}px container; consent text is ${Math.round(eRect.width)}px wide — symmetric clip attack. justify-content:${ps.justifyContent}` });
}
}
/* Also check for consent element that has very small visible rect but large scroll width */
if (s.overflow === 'hidden' || s.overflowX === 'hidden') {
const scrollWidth = el.scrollWidth;
const clientWidth = el.clientWidth;
if (scrollWidth > clientWidth * 3 && clientWidth < 100) {
findings.push({ id: 'SA-CSS-JUSCONT-004', severity: 'medium',
message: `Consent element has scrollWidth (${scrollWidth}px) >> clientWidth (${clientWidth}px) with overflow:hidden — potential center-clip attack.` });
}
}
}
return findings;
}
Browser support for safe/unsafe keywords varies: The safe and unsafe keywords are part of CSS Box Alignment Level 3. As of 2024, Firefox supports them natively; Chrome and Safari support safe center but may not fully implement unsafe center semantics in all contexts. However, the clipping behavior of justify-content: center with an undersized container and overflow: hidden produces the same clip effect even without the unsafe keyword in implementations that default to unsafe centering behavior.
SkillAudit findings for CSS justify-content consent attacks
justify-content: flex-end or end on a flex container with direction: rtl; consent element rendered at x < 0 (off-screen to the left). RTL direction inverts the meaning of "end", pushing consent to the physical left edge. Detected by checking direction: rtl with justifyContent end values and consent element's negative x position.justify-content: space-between in a container wider than the viewport; consent element rendered at x > viewport width. The oversized container plus space-between places consent at the far right edge, off-screen. Detected by comparing container width to viewport width and consent element's off-screen x position.justify-content: end and a narrow parent with overflow: hidden; consent in a rightmost column that falls beyond the parent's visible area. Detected by checking grid containers with end alignment where consent columns are off-screen right.unsafe center alignment (or centering in an undersized container) with overflow: hidden; consent text is wider than the visible container and is clipped symmetrically on both sides, rendering no complete words. Detected by comparing scroll width to client width on consent elements with overflow:hidden.Related MCP consent attack research
- CSS align-content attacks — cross-axis line displacement
- CSS place-content attacks — combined justify + align content shorthand
- CSS writing-mode attacks — block/inline axis reorientation
- CSS overflow attacks — consent clipping via overflow:hidden
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
Audit your MCP server for justify-content consent displacement attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-JUSCONT findings.