Security reference · CSS injection · Flex/Grid attacks · Cross-axis item alignment
MCP server CSS align-items security
CSS align-items sets the default cross-axis alignment for all items within a single flex line or grid row. Unlike align-content which distributes multiple lines, align-items controls how each item is positioned within its line's cross-axis space. MCP servers exploit align-items to push consent items to the cross-axis end of a constrained container — sending them beyond the visible area when overflow: hidden clips the cross axis. This is an item-level attack operating within a single flex line or grid row, distinct from multi-line align-content displacement.
align-items attack surface
| Attack configuration | align-items value | Supporting properties | Effect on consent |
|---|---|---|---|
| Column flex end-push | flex-end / end | flex-direction: column; height: Xpx; overflow: hidden | All items packed toward the bottom of the column container; if total item height exceeds container height, items overflow at the bottom and are clipped |
| Stretch with zero container height | stretch | height: 0; overflow: hidden on flex container | align-items:stretch expands items to fill container cross-axis; zero-height container means all items collapse to zero height |
| Baseline displacement | baseline | Tall sibling with large font; consent aligned to bottom of sibling baseline | Baseline alignment anchors consent to the text baseline of the tallest sibling; if sibling is artificially tall, consent is displaced below visible area |
| Grid self-end in oversized cell | self-end / end | Grid container with explicit tall row height; consent in row with height > viewport | align-items:end positions consent at the bottom of its grid cell; if the cell is tall, consent bottom edge is off-screen |
align-items applies within a single line — not across lines: In a row-direction flex container, align-items aligns items vertically within that single row (cross axis = vertical). In a column-direction flex container, align-items aligns items horizontally within the column (cross axis = horizontal). The most effective consent-hiding attacks use flex-direction: column with align-items: flex-end to displace consent horizontally off the right edge — or flex-direction: column; overflow: hidden combined with pushing content vertically beyond a height cap.
Attack 1: align-items: flex-end in column flex — cross-axis horizontal displacement
In a flex-direction: column container, the cross axis is horizontal. align-items: flex-end pushes all items to the right edge of the container (or left in RTL). By making the container narrower than the consent element's content width, the consent item is pushed to the right edge and its text overflows rightward beyond the container boundary, clipped by overflow: hidden. The install form items (with explicit align-self: flex-start) remain on the left and are visible:
/* Malicious CSS — SA-CSS-ALITM-001 */
.mcp-install-column {
display: flex;
flex-direction: column;
align-items: flex-end; /* cross-axis = horizontal; items pushed to right */
width: 300px;
overflow: hidden;
}
.mcp-install-form {
align-self: flex-start; /* override: install form stays at left — visible */
width: 100%;
}
.mcp-consent-disclosure {
/* inherits align-items:flex-end */
/* width: auto — fits content; right-aligned within 300px container */
/* if content wider than container, overflow clips rightward */
/* or use width:0 + flex-end: consent is a 0-width block at right edge */
width: 0; /* zero width; align-items:flex-end positions it at right edge */
/* text content overflows to the right, outside container */
}
/* Detection */
function detectAlignItemsFlexEndColumnOverflow() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
if (s.flexDirection !== 'column' && s.flexDirection !== 'column-reverse') continue;
if (!/flex-end|end/.test(s.alignItems)) continue;
if (s.overflow !== 'hidden' && s.overflow !== 'clip') 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.right > elRect.right + 2) {
findings.push({ id: 'SA-CSS-ALITM-001', severity: 'high',
message: `Column flex container with align-items:${s.alignItems} and overflow:hidden — consent item at x=${Math.round(ckRect.left)}, width=${Math.round(ckRect.width)}px. Potential horizontal cross-axis overflow displacement.` });
}
}
}
return findings;
}
Attack 2: align-items: stretch with zero-height container
align-items: stretch (the default) expands each flex item to fill the container's cross-axis size. In a row-direction flex container, this expands items vertically to the container height. If the container height is set to 0 with overflow: hidden, all items are stretched to zero height and clipped. This attack uses the default value — align-items: stretch is the initial value, so MCP doesn't need to set it explicitly — just set the container height to zero:
/* Malicious CSS — SA-CSS-ALITM-002 */
.mcp-install-row {
display: flex;
flex-direction: row;
align-items: stretch; /* default — items expand to container height */
height: 0; /* container height: 0px */
overflow: hidden; /* clips all content including zero-height items */
/* Note: align-items:stretch is the initial value; this property may not appear
in computed style as 'stretch' in all browsers — check for absence of other values */
}
/* Both install form and consent are collapsed to zero height.
Attack uses a second visible container for the install form
and this hidden container only for consent. */
.mcp-visible-form {
/* separate non-grid container — install form is fully visible here */
}
.mcp-hidden-consent {
/* placed in the zero-height align-items:stretch container */
/* computed height: 0px; visible: false */
}
/* Detection */
function detectAlignItemsStretchZeroHeight() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
const elRect = el.getBoundingClientRect();
if (elRect.height > 2) continue; /* container must be near-zero height */
if (s.overflow !== 'hidden' && s.overflow !== 'clip') continue;
const children = [...el.children];
const consentKids = children.filter(c =>
/consent|disclosure|terms|privacy/i.test(c.textContent || '')
);
if (consentKids.length > 0) {
findings.push({ id: 'SA-CSS-ALITM-002', severity: 'critical',
message: `Flex container height is ${Math.round(elRect.height)}px with overflow:hidden — consent items stretched to zero height via align-items:stretch (default). All content clipped.` });
}
}
return findings;
}
Attack 3: align-items: baseline — tall sibling baseline displacement
When align-items: baseline is set, items align their text baseline with the tallest sibling's baseline. An MCP server injects a sibling element with font-size: 500px and visibility: hidden (not hidden from layout — still occupies space). The baseline of this tall element is far below the container top. Consent aligned to this baseline is positioned far down in the container. If the container has a fixed height and overflow: hidden, consent is clipped below the visible area:
/* Malicious CSS — SA-CSS-ALITM-003 */
.mcp-install-row {
display: flex;
flex-direction: row;
align-items: baseline; /* items align to shared text baseline */
height: 80px;
overflow: hidden;
}
.mcp-spacer-text {
/* Injected by MCP: giant invisible text sets baseline far down */
font-size: 500px; /* 500px cap height; baseline ~420px from top */
visibility: hidden; /* invisible but occupies layout space */
width: 0; /* zero width — layout spacer only */
overflow: hidden;
}
.mcp-install-form {
/* aligned to the 420px baseline but container is only 80px */
/* install form text baseline at 420px below container top */
/* container top portion shows install form if font-size is moderate */
}
.mcp-consent-disclosure {
/* also aligned to 420px baseline — consent text baseline at y=420px */
/* 80px container clips everything above 80px — consent is below the clip */
}
/* Detection */
function detectAlignItemsBaselineDisplacement() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
if (!/baseline/.test(s.alignItems)) continue;
if (s.overflow !== 'hidden' && s.overflow !== 'clip') continue;
const children = [...el.children];
const largeFontSiblings = children.filter(c => {
const cs = getComputedStyle(c);
return parseFloat(cs.fontSize) > 100;
});
const consentKids = children.filter(c =>
/consent|disclosure|terms|privacy/i.test(c.textContent || '')
);
if (largeFontSiblings.length > 0 && consentKids.length > 0) {
const ckRect = consentKids[0].getBoundingClientRect();
if (ckRect.height < 2 || ckRect.top > el.getBoundingClientRect().bottom) {
findings.push({ id: 'SA-CSS-ALITM-003', severity: 'high',
message: `Flex container with align-items:baseline has large-font sibling (${largeFontSiblings.length} found) and consent element with height:${Math.round(ckRect.height)}px. Baseline displacement may clip consent below container.` });
}
}
}
return findings;
}
Attack 4: align-items: end in grid — consent at bottom of oversized row
In a CSS Grid container, align-items: end positions each grid item at the bottom of its grid cell. An MCP server creates a grid with a first row (grid-template-rows: 100px 2000px) where the install form is in row 1 (100px, visible) and consent is in row 2 (2000px, deliberately oversized). With align-items: end, consent is anchored to the bottom of the 2000px row — 2000px below its row start, which is already off-screen. The container uses overflow: hidden or the row simply starts off-screen:
/* Malicious CSS — SA-CSS-ALITM-004 */
.mcp-install-grid {
display: grid;
grid-template-rows: 100px 2000px; /* row 2 is 2000px tall */
align-items: end; /* items anchor to bottom of their cell */
overflow: hidden;
height: 100px; /* container only shows row 1 */
}
.mcp-install-form {
grid-row: 1;
/* cell height: 100px; align-items:end positions bottom of form at y=100px */
/* visible within the 100px container */
}
.mcp-consent-disclosure {
grid-row: 2;
/* cell starts at y=100px, is 2000px tall */
/* align-items:end positions consent bottom at y = 100 + 2000 = 2100px */
/* consent top = 2100 - consent_height; far below the 100px visible area */
}
/* Detection */
function detectGridAlignItemsEndOversizedRow() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
if (!/\bend\b|self-end/.test(s.alignItems)) 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.top > window.innerHeight || ckRect.height < 2) {
findings.push({ id: 'SA-CSS-ALITM-004', severity: 'critical',
message: `Grid container with align-items:${s.alignItems} — consent positioned at y=${Math.round(ckRect.top)}px (${ckRect.top > window.innerHeight ? 'off-screen' : 'near-zero height'}). Oversized grid row with end alignment may place consent off-screen.` });
}
}
}
return findings;
}
align-items vs. align-self — detection scope difference: align-items sets the default for all items in the container; align-self overrides it per item. Consent-hiding attacks using align-items affect every item in the container unless overridden. This means the install form must also use align-self to restore its intended alignment, creating a detectable pattern: when align-items is flex-end or end and some children have explicit align-self: flex-start (the install form) while consent has no align-self override (inheriting the hostile value), this asymmetry is a strong signal.
SkillAudit findings for CSS align-items consent attacks
align-items: flex-end or end on a flex-direction: column container with overflow: hidden; consent item has computed width < 4px or extends beyond container right edge. Cross-axis horizontal displacement pushes consent off the right side of the container.overflow: hidden, and consent child elements. Default align-items: stretch collapses all items to zero height matching the zero-height container. Consent is present in DOM but clipped to invisible.align-items: baseline, overflow: hidden, and a sibling element with font-size > 100px; consent item has computed height < 2px or top position exceeds container bottom. Tall sibling baseline displacement pushes consent below the visible container area.align-items: end or self-end; consent grid item positioned at top > window.innerHeight or has computed height < 2px. Oversized grid row with end alignment anchors consent to the bottom of a cell that starts or ends off-screen.Related MCP consent attack research
- CSS align-content attacks — multi-line cross-axis displacement
- CSS align-self attacks — per-item cross-axis self-alignment override
- CSS justify-items attacks — grid inline-axis item alignment
- CSS flex-shrink attacks — asymmetric shrink factor collapse
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
Audit your MCP server for align-items consent displacement attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-ALITM findings.