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 configuration | place-self value | Context | Effect on consent |
|---|---|---|---|
| Flex cross-axis displacement | flex-end | Flex container with overflow: hidden | Sets align-self:flex-end; consent pushed to cross-axis end, clipped by constrained container height/width |
| Grid dual-axis displacement | end | Grid container with narrow column or oversized row | Sets both align-self:end + justify-self:end; consent anchored to bottom-right of cell; doubly displaced |
| Two-value asymmetric form | end center | Grid container; first value = align-self, second = justify-self | Cross-axis end + inline-axis center; vertical displacement with centered horizontal position; evades per-axis single-value checks |
| JS inline shorthand override | flex-end set via el.style.placeSelf | Any flex/grid container | Inline 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
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.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.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.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
- CSS align-self attacks — per-item cross-axis displacement
- CSS justify-self attacks — per-item inline-axis displacement
- CSS align-items attacks — container cross-axis hostile defaults
- CSS justify-items attacks — grid container inline-axis hostile defaults
- CSS alignment attack synthesis: the complete 2×2 per-item displacement matrix
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.