Security reference · CSS injection · Shorthand alignment · Consent displacement

MCP server CSS place-self security

CSS place-self is a shorthand property that sets align-self and justify-self in a single declaration. In a flex container, it applies only the cross-axis component (align-self); in a grid container, both components take effect simultaneously. The shorthand creates a double evasion opportunity: auditors that check only longhand computed properties may scan alignSelf and justifySelf separately but miss the shorthand's simultaneous dual-axis effect on layout, and the two-value syntax (place-self: end center) can set a hostile value on one axis while a benign value on the other to confuse per-axis threshold checks.

place-self attack surface

Attack configurationplace-self valueContextEffect on consent
Flex cross-axis displacementflex-endFlex container with overflow: hiddenSets align-self:flex-end; consent pushed to cross-axis end, clipped by constrained container height/width
Grid dual-axis displacementendGrid container with narrow column or oversized rowSets both align-self:end + justify-self:end; consent anchored to bottom-right of cell; doubly displaced
Two-value asymmetric formend centerGrid container; first value = align-self, second = justify-selfCross-axis end + inline-axis center; vertical displacement with centered horizontal position; evades per-axis single-value checks
JS inline shorthand overrideflex-end set via el.style.placeSelfAny flex/grid containerInline shorthand override bypasses cascade; MutationObserver watching only align-self or justify-self attribute changes misses the shorthand property name

Shorthand evasion of longhand auditors: When a MutationObserver watches for inline style changes to align-self or justify-self, setting el.style.placeSelf = 'flex-end' writes the shorthand to the inline style — not the longhand properties. The mutation fires on the style attribute, but the attribute value is place-self: flex-end. An auditor checking el.style.alignSelf after the mutation may see the expanded computed value, but a regex checking the raw attribute string for "align-self" will miss it.

Attack 1: place-self: flex-end in flex container — cross-axis displacement via shorthand

In a flex container, place-self: flex-end acts identically to align-self: flex-end — the justify-self component is ignored in flex layout. The shorthand provides no new capability in flexbox, but it hides the actual mechanism: an audit that scans for the string align-self in the stylesheet text will miss a rule written as place-self, even though the computed effect is the same:

/* Malicious CSS — SA-CSS-PLSLF-001 */
.mcp-install-column {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  width: 300px;
  overflow: hidden;
}

.mcp-consent-disclosure {
  place-self: flex-end;  /* shorthand: sets align-self:flex-end in flex context */
                         /* audit scanning for "align-self" in stylesheet text misses this */
  width: 0;
}

/* Stylesheet-text detection fails:
   /align-self/.test(stylesheet) → false (no literal "align-self" in source)
   /place-self/.test(stylesheet) → true (but many auditors don't check this)

   Computed-value detection succeeds:
   getComputedStyle(el).alignSelf → "flex-end" (browser expands shorthand)
   Always use computed style, never raw stylesheet text, for detection */

function detectPlaceSelfFlex() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'flex' && s.display !== 'inline-flex') continue;
    if (s.overflow !== 'hidden' && s.overflow !== 'clip') continue;
    for (const child of el.children) {
      if (!/consent|disclosure|terms|privacy/i.test(child.textContent || '')) continue;
      const cs = getComputedStyle(child);
      if (/flex-end|end|self-end/.test(cs.alignSelf)) {
        const rect = child.getBoundingClientRect();
        if (rect.width < 2 || rect.height < 2 || rect.top > window.innerHeight) {
          findings.push({ id: 'SA-CSS-PLSLF-001', severity: 'high',
            message: `Consent element computed align-self:${cs.alignSelf} in flex container — may originate from place-self shorthand. Check: el.style.placeSelf=${child.style.placeSelf || 'not set inline'}. Rect: w=${Math.round(rect.width)},h=${Math.round(rect.height)}.` });
        }
      }
    }
  }
  return findings;
}

Attack 2: place-self: end in grid container — simultaneous dual-axis displacement

In a CSS Grid container, place-self: end expands to both align-self: end (cross-axis, block) and justify-self: end (inline axis). This means consent is simultaneously displaced to the block-end (bottom of its row) and the inline-end (right of its column) of its grid cell. In a grid cell that is either tall or narrow, the dual-axis displacement compounds: consent is pushed to the corner of the cell that is furthest from the install form:

/* Malicious CSS — SA-CSS-PLSLF-002 */
.mcp-install-grid {
  display: grid;
  grid-template-columns: 300px 0px;  /* second column: zero width */
  grid-template-rows: auto 2000px;   /* second row: 2000px tall */
}

.mcp-install-form  { grid-area: 1 / 1; }  /* visible: row 1, column 1 (300px wide) */
.mcp-consent-disclosure {
  grid-area: 2 / 2;    /* row 2 (2000px below) + column 2 (zero width) */
  place-self: end;     /* expands to: align-self:end + justify-self:end */
                       /* anchored to bottom-right corner of a 2000px tall zero-width cell */
                       /* doubly displaced: off-screen vertically + overflowing right from zero-width anchor */
}

/* Detection: check both computed axis values and validate against rect */
function detectPlaceSelfGrid() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    for (const child of el.children) {
      if (!/consent|disclosure|terms|privacy/i.test(child.textContent || '')) continue;
      const cs = getComputedStyle(child);
      const rect = child.getBoundingClientRect();
      const alignHostile  = /end|flex-end|self-end/.test(cs.alignSelf);
      const justifyHostile = /end|flex-end|self-end|right/.test(cs.justifySelf);
      if (alignHostile && justifyHostile) {
        const isHidden = rect.width < 2 || rect.height < 2
          || rect.top > window.innerHeight || rect.right < 0;
        findings.push({ id: 'SA-CSS-PLSLF-002', severity: isHidden ? 'critical' : 'high',
          message: `Consent element has both align-self:${cs.alignSelf} + justify-self:${cs.justifySelf} in grid — dual-axis displacement. May originate from place-self shorthand. Hidden: ${isHidden}.` });
      }
    }
  }
  return findings;
}

Attack 3: place-self: end center — two-value asymmetric displacement

The two-value form of place-self sets align-self from the first value and justify-self from the second. This allows a hostile MCP server to apply a different value to each axis: place-self: end center sets align-self: end (block-end displacement) while justify-self: center appears safe on its own. An audit that checks whether any axis uses an end-value will correctly flag this — but an audit that checks only the inline axis (looking for horizontal off-screen displacement) will miss the vertical displacement from the cross-axis component:

/* Malicious CSS — SA-CSS-PLSLF-003 */
.mcp-install-grid {
  display: grid;
  grid-template-rows: auto 100vh;  /* row 2: one full viewport height */
  overflow: hidden;
}

.mcp-install-form  { grid-row: 1; }  /* visible: first row */
.mcp-consent-disclosure {
  grid-row: 2;
  place-self: end center;  /* align-self:end + justify-self:center */
                           /* vertical: anchored to bottom of 100vh row = 2 viewport heights below top */
                           /* horizontal: centered within the column — looks safe on inline axis */
                           /* consent is entirely off-screen vertically */
}

/* Detection: each axis is checked independently — either hostile triggers a finding */
function detectPlaceSelfAsymmetric() {
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    const s = getComputedStyle(el);
    if (s.display !== 'grid' && s.display !== 'inline-grid') continue;
    for (const child of el.children) {
      if (!/consent|disclosure|terms|privacy/i.test(child.textContent || '')) continue;
      const cs = getComputedStyle(child);
      const rect = child.getBoundingClientRect();
      const alignHostile  = /end|flex-end|self-end/.test(cs.alignSelf);
      const justifyHostile = /end|flex-end|self-end|right/.test(cs.justifySelf);
      if (alignHostile || justifyHostile) {
        findings.push({ id: 'SA-CSS-PLSLF-003', severity: 'high',
          message: `Consent child: align-self=${cs.alignSelf}, justify-self=${cs.justifySelf}. At least one axis is hostile — may be two-value place-self shorthand. Rect: top=${Math.round(rect.top)}, left=${Math.round(rect.left)}, w=${Math.round(rect.width)}, h=${Math.round(rect.height)}.` });
      }
    }
  }
  return findings;
}

Attack 4: JS inline place-self shorthand — MutationObserver shorthand evasion

When JavaScript sets el.style.placeSelf = 'flex-end', the browser writes the shorthand property name to the inline style attribute. A MutationObserver watching the style attribute will fire — but the observer callback receives the raw attribute string. An auditor parsing that string for the substrings "align-self" or "justify-self" will not find them; the string contains "place-self" instead. Only reading the computed style (which expands the shorthand) or checking for "place-self" explicitly will detect the change:

/* Malicious JS — SA-CSS-PLSLF-004 */
document.querySelector('.mcp-install-button').addEventListener('mouseover', () => {
  const consent = document.querySelector('.mcp-consent-disclosure');
  consent.style.placeSelf = 'flex-end';
  /* el.style attribute becomes: "place-self: flex-end;"
     el.style.alignSelf  → "flex-end" (browser expands shorthand in computed style)
     el.style.placeSelf  → "flex-end" (shorthand property)
     el.getAttribute('style') → "place-self: flex-end;" — does NOT contain "align-self" */
});

/* Vulnerable detection (misses the shorthand path): */
observer.observe(consent, { attributes: true, attributeFilter: ['style'] });
/* Callback checks: if (el.getAttribute('style').includes('align-self')) — MISSES IT */

/* Correct detection (reads computed style after mutation): */
const observer = new MutationObserver((mutations) => {
  for (const mut of mutations) {
    if (mut.attributeName !== 'style') continue;
    const el = mut.target;
    const cs = getComputedStyle(el);
    /* Use computed style — shorthand is already expanded here */
    if (/flex-end|end|self-end/.test(cs.alignSelf) || /flex-end|end|self-end|right/.test(cs.justifySelf)) {
      const rect = el.getBoundingClientRect();
      findings.push({ id: 'SA-CSS-PLSLF-004', severity: 'critical',
        message: `Consent element style attribute changed. Computed: alignSelf=${cs.alignSelf}, justifySelf=${cs.justifySelf}. Raw inline: ${el.getAttribute('style')}. Shorthand place-self may be the source.` });
    }
  }
});
document.querySelectorAll('[class*="consent"],[class*="disclosure"],[class*="terms"]').forEach(el =>
  observer.observe(el, { attributes: true, attributeFilter: ['style'] })
);

Always use computed style for alignment detection: Browsers expand shorthand properties like place-self, place-items, and place-content into their longhand computed values. getComputedStyle(el).alignSelf returns the correct value regardless of whether the source was a longhand declaration, a shorthand, or a JS shorthand override. Avoid parsing raw stylesheet text or inline style attribute strings for alignment keywords — use computed style exclusively.

SkillAudit findings for CSS place-self consent attacks

HighSA-CSS-PLSLF-001 — Flex container child with computed align-self: flex-end, end, or self-end matching consent patterns; inline style contains place-self shorthand rather than align-self longhand. Container has overflow: hidden and consent is near-zero dimension or off-screen.
CriticalSA-CSS-PLSLF-002 — Grid container child with both computed align-self and justify-self set to hostile end-values simultaneously, matching consent patterns. Dual-axis displacement indicates place-self: end shorthand. Consent positioned at bottom-right corner of a narrow or oversized grid cell.
HighSA-CSS-PLSLF-003 — Grid container child where either align-self or justify-self is hostile while the other is benign; two-value place-self form creates asymmetric displacement on one axis while masking the attack on the other. At least one axis shows off-screen or zero-dimension positioning.
CriticalSA-CSS-PLSLF-004 — Consent element inline style attribute changes after page load; computed alignSelf or justifySelf becomes hostile but the raw style attribute string contains "place-self" rather than longhand names. Shorthand-based MutationObserver evasion detected.

Related MCP consent attack research

Audit your MCP server for place-self shorthand consent displacement attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-PLSLF findings. SkillAudit uses computed style — not stylesheet text — for shorthand-aware detection.