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 configurationalign-self value on consentSupporting propertiesEffect on consent
Column flex horizontal displacementflex-end / endflex-direction: column; width: Xpx; overflow: hidden; consent width exceeds column widthOnly consent item pushed to right (cross-axis end) of column container; install form items stay left; consent overflows right edge
Row flex vertical displacementflex-end / endflex-direction: row; height: Xpx; overflow: hidden; container height shorter than consentOnly consent item pushed to bottom (cross-axis end) of row container; consent bottom edge exceeds container height; clipped
Grid cross-axis oversized rowself-end / endGrid row with explicit height > viewport; overflow: hidden on ancestorOnly consent anchored to bottom of its grid row; row extends off-screen; consent bottom at row bottom is off-screen
JS-triggered align-self changeInitially auto → changed to flex-endInline style or class change after 1–2 seconds or on button hoverConsent 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

HighSA-CSS-ALSLF-001 — Flex or grid container child with 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.
HighSA-CSS-ALSLF-002 — Row-direction flex container child with 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.
CriticalSA-CSS-ALSLF-003 — Grid child with 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.
CriticalSA-CSS-ALSLF-004 — Consent element's inline 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

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.