Security reference · CSS injection · Flex/Grid attacks · Per-item cross-axis alignment
MCP server CSS align-self security
CSS align-self overrides the container's align-items value for a single specific item, setting its cross-axis alignment independently from all other items. This per-item scope makes it the most surgically precise cross-axis consent-hiding tool: an MCP server can leave all install form items at their default align-self: auto (inheriting a safe align-items value) while setting align-self: flex-end or align-self: self-end exclusively on the consent element — displacing only consent to the cross-axis end without affecting the install form layout. The asymmetry is legible in CSS but evades blanket container-level checks.
align-self attack surface
| Attack configuration | align-self value on consent | Supporting properties | Effect on consent |
|---|---|---|---|
| Column flex horizontal displacement | flex-end / end | flex-direction: column; width: Xpx; overflow: hidden; consent width exceeds column width | Only consent item pushed to right (cross-axis end) of column container; install form items stay left; consent overflows right edge |
| Row flex vertical displacement | flex-end / end | flex-direction: row; height: Xpx; overflow: hidden; container height shorter than consent | Only consent item pushed to bottom (cross-axis end) of row container; consent bottom edge exceeds container height; clipped |
| Grid cross-axis oversized row | self-end / end | Grid row with explicit height > viewport; overflow: hidden on ancestor | Only consent anchored to bottom of its grid row; row extends off-screen; consent bottom at row bottom is off-screen |
| JS-triggered align-self change | Initially auto → changed to flex-end | Inline style or class change after 1–2 seconds or on button hover | Consent visible at load time; align-self changed to flex-end after audit window; consent displaced at moment of user interaction |
Surgical targeting — the key risk of align-self attacks: When an audit scans a container-level property like align-items, a single check on the container element is sufficient. align-self requires scanning every child element of every flex or grid container — the attack surface is O(n) across all DOM elements, not O(containers). SkillAudit scans all flex and grid children for align-self values that differ from the container default and checks whether consent-containing elements are the targets of the hostile per-item value.
Attack 1: align-self: flex-end on consent in column flex — targeted horizontal displacement
In a flex-direction: column container, the cross axis is horizontal. align-self: flex-end on the consent element positions only consent at the right edge of the container, while all other items remain at the default cross-axis position (left, via align-items: flex-start or stretch). The consent element's content — wider than the narrow column — overflows rightward and is clipped by overflow: hidden:
/* Malicious CSS — SA-CSS-ALSLF-001 */
.mcp-install-column {
display: flex;
flex-direction: column; /* cross axis = horizontal */
align-items: flex-start; /* all items start-aligned by default (left) */
width: 300px;
overflow: hidden;
}
/* Install form items: inherit align-items:flex-start — visible at left */
.mcp-install-title { /* align-self: auto → flex-start: left-aligned, visible */ }
.mcp-install-input { /* align-self: auto → flex-start: left-aligned, visible */ }
.mcp-install-button { /* align-self: auto → flex-start: left-aligned, visible */ }
.mcp-consent-disclosure {
align-self: flex-end; /* ONLY consent: right-aligned (cross-axis end) */
width: 0; /* zero width; positioned at x=300px (right edge) */
/* content overflows rightward from x=300px */
/* overflow:hidden clips it to invisible */
}
/* Detection: scan each flex/grid child for align-self that differs from container default */
function detectAlignSelfTargetedDisplacement() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex' &&
s.display !== 'grid' && s.display !== 'inline-grid') 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 cs = getComputedStyle(ck);
if (/flex-end|end|self-end/.test(cs.alignSelf)) {
const ckRect = ck.getBoundingClientRect();
const nonConsentKids = children.filter(c => !consentKids.includes(c));
const nonConsentEndAligned = nonConsentKids.filter(c =>
/flex-end|end|self-end/.test(getComputedStyle(c).alignSelf)
);
if (nonConsentEndAligned.length < nonConsentKids.length * 0.5) {
findings.push({ id: 'SA-CSS-ALSLF-001', severity: 'high',
message: `Consent element has align-self:${cs.alignSelf} (cross-axis end) while most siblings do not — targeted per-item displacement. Container: ${s.flexDirection || s.display}. Consent rect: w=${Math.round(ckRect.width)},top=${Math.round(ckRect.top)}.` });
}
}
}
}
return findings;
}
Attack 2: align-self: flex-end on consent in row flex — vertical displacement below clip
In a flex-direction: row container, the cross axis is vertical. align-self: flex-end on consent positions it at the bottom of the row container's height. If the container has a fixed height shorter than the combined form + consent height, and overflow: hidden clips vertically, consent is placed at the bottom of a container that isn't tall enough to show it — while the install form items at the default align-self: flex-start are visible at the top:
/* Malicious CSS — SA-CSS-ALSLF-002 */
.mcp-install-row {
display: flex;
flex-direction: row; /* cross axis = vertical */
align-items: flex-start; /* default: items at top */
height: 60px;
overflow: hidden;
}
/* Install form and button: inherit flex-start → visible at top */
.mcp-install-input { /* top-aligned */ }
.mcp-install-button { /* top-aligned */ }
.mcp-consent-disclosure {
align-self: flex-end; /* bottom of 60px container */
/* consent bottom = 60px; if consent height > 60px, part or all is clipped */
/* More effective variant: consent height = 40px, but row has multiple
siblings that push the line height to 60px — consent at bottom of 60px,
positioned at y=20 to y=60. Visible but at the bottom. */
/* With height:0 on consent: align-self:flex-end positions a 0-height element
at the bottom edge — content overflows downward and is clipped. */
height: 0; /* zero height at flex-end position = bottom edge = 60px */
/* text overflows below 60px → clipped by overflow:hidden */
}
/* Detection */
function detectAlignSelfFlexEndRowVertical() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
if (s.flexDirection !== 'row' && s.flexDirection !== 'row-reverse') 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 cs = getComputedStyle(ck);
if (!/flex-end|end|self-end/.test(cs.alignSelf)) continue;
const ckRect = ck.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
if (ckRect.height < 2 || ckRect.bottom > elRect.bottom + 2) {
findings.push({ id: 'SA-CSS-ALSLF-002', severity: 'high',
message: `Row flex consent has align-self:${cs.alignSelf} — height=${Math.round(ckRect.height)}px, bottom=${Math.round(ckRect.bottom)}px vs container bottom=${Math.round(elRect.bottom)}px. Vertical displacement by per-item align-self.` });
}
}
}
return findings;
}
Attack 3: align-self: self-end in grid — oversized row bottom anchor
In a CSS Grid container, align-self: self-end anchors the consent item to the bottom of its grid row cell. An MCP server creates a grid where the install form is in row 1 (visible height) and consent is in row 2 (explicitly sized at 2000px or using 100vh). With align-self: self-end, consent is placed at the bottom of the 2000px cell — 2000px below where the cell starts, which is already below the viewport. The install form in row 1 is completely visible; consent is completely off-screen:
/* Malicious CSS — SA-CSS-ALSLF-003 */
.mcp-install-grid {
display: grid;
grid-template-rows: auto 2000px; /* row 2 is 2000px tall */
}
.mcp-install-form {
grid-row: 1;
/* align-self defaults to stretch/start — visible in auto-height row */
}
.mcp-consent-disclosure {
grid-row: 2;
align-self: self-end; /* anchored to bottom of 2000px cell */
/* Cell row 2 starts at y = height of row 1 (say 80px) */
/* Cell bottom = 80 + 2000 = 2080px */
/* align-self:self-end: consent bottom = 2080px */
/* consent is at ~2060-2080px — far off-screen */
}
/* Detection */
function detectAlignSelfGridSelfEnd() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const s = getComputedStyle(el);
if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
const children = [...el.children];
const consentKids = children.filter(c =>
/consent|disclosure|terms|privacy/i.test(c.textContent || '')
);
for (const ck of consentKids) {
const cs = getComputedStyle(ck);
if (!/self-end|end|flex-end/.test(cs.alignSelf)) continue;
const ckRect = ck.getBoundingClientRect();
if (ckRect.top > window.innerHeight || ckRect.height < 2) {
findings.push({ id: 'SA-CSS-ALSLF-003', severity: 'critical',
message: `Grid consent item has align-self:${cs.alignSelf} — positioned at y=${Math.round(ckRect.top)}px (${ckRect.top > window.innerHeight ? 'off-screen' : 'near-zero height'}). Oversized grid row with per-item self-end alignment displaces consent off screen.` });
}
}
}
return findings;
}
Attack 4: JS-triggered align-self change on button interaction
The most evasion-resistant align-self attack: consent initially has align-self: auto (normal, visible). When the user hovers or focuses the install button, MCP JavaScript sets el.style.alignSelf = 'flex-end' on the consent element inline — displacing it to the cross-axis end at the exact moment the user is deciding to click. The install button interaction and the consent collapse are simultaneous; the user moves their attention toward the install button and loses consent visibility in the same gesture:
/* Malicious CSS — SA-CSS-ALSLF-004 */
/* Initial state: consent is visible */
.mcp-consent-disclosure {
/* align-self: auto — inherits align-items, which is flex-start: visible */
}
/* MCP JavaScript — triggered on user intent to install */
document.querySelector('.mcp-install-button').addEventListener('mouseover', () => {
const consent = document.querySelector('.mcp-consent-disclosure');
consent.style.alignSelf = 'flex-end'; /* inline style: immediate per-item override */
/* The flex container must have overflow:hidden for this to clip consent */
/* Or combine with height:0 on the same event */
});
/* More subtle variant: use CSS variable */
document.querySelector('.mcp-install-button').addEventListener('focus', () => {
document.documentElement.style.setProperty('--consent-self', 'flex-end');
});
/* CSS using the variable */
.mcp-consent-disclosure {
align-self: var(--consent-self, auto); /* auto until JS sets the variable */
}
/* Detection: MutationObserver on consent elements watching inline style changes */
function detectDeferredAlignSelfChange() {
const findings = [];
const consentSelectors = '[class*="consent"],[class*="disclosure"],[class*="terms"]';
const consentEls = document.querySelectorAll(consentSelectors);
const observer = new MutationObserver((mutations) => {
for (const mut of mutations) {
if (mut.type !== 'attributes' || mut.attributeName !== 'style') continue;
const el = mut.target;
const cs = getComputedStyle(el);
if (/flex-end|end|self-end/.test(cs.alignSelf)) {
const rect = el.getBoundingClientRect();
findings.push({ id: 'SA-CSS-ALSLF-004', severity: 'critical',
message: `Consent element inline style changed: align-self is now ${cs.alignSelf}. Position: top=${Math.round(rect.top)}, height=${Math.round(rect.height)}. Deferred per-item displacement after user interaction.` });
}
}
});
consentEls.forEach(el => observer.observe(el, { attributes: true, attributeFilter: ['style'] }));
return findings;
}
align-self requires per-element scanning — container checks are insufficient: An audit tool that only checks container-level CSS properties (align-items, align-content, justify-content) will always miss align-self attacks because align-self is set on the item, not on the container. A malicious MCP stylesheet can set benign container-level alignment while using align-self on only the consent element to cause displacement. SkillAudit scans all elements matching consent-disclosure patterns for hostile align-self values independently of container properties.
SkillAudit findings for CSS align-self consent attacks
align-self: flex-end, end, or self-end matching consent patterns, while less than 50% of sibling elements share the same alignment — targeted per-item displacement with asymmetric sibling alignment. Container has overflow: hidden.align-self: flex-end; consent item has computed height < 2px or bottom position exceeds container bottom. Per-item end-alignment in row flex displaces consent vertically below the container's height clip.align-self: self-end or end; consent positioned at top > window.innerHeight or height < 2px. Per-item grid self-end alignment anchors consent to the bottom of an oversized grid row that extends off-screen.style attribute changes to add align-self: flex-end or equivalent after page load, triggered by button interaction events. MutationObserver detects the inline style mutation. Deferred per-item displacement coincides with user install gesture.Related MCP consent attack research
- CSS align-items attacks — container-level cross-axis alignment
- CSS align-content attacks — multi-line cross-axis line distribution
- CSS justify-self attacks — per-item inline-axis self-alignment
- CSS flex-shrink attacks — asymmetric per-item size collapse
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
Audit your MCP server for align-self consent displacement attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-ALSLF findings.