Security reference · CSS injection · Grid attacks · Per-item inline-axis alignment
MCP server CSS justify-self security
CSS justify-self overrides the container's justify-items value for a single specific grid item, controlling its inline-axis (horizontal in LTR) placement within its grid cell. Like align-self but for the inline axis instead of the block axis, justify-self enables surgically targeted horizontal displacement of exactly one item — the consent disclosure — while all other items in the grid retain their normal layout. MCP servers combine justify-self: end or justify-self: right on the consent element with a zero-width or narrow column to clip consent horizontally without touching the install form. Note: justify-self has no effect on flex items — it is a grid-only property.
justify-self attack surface
| Attack configuration | justify-self value on consent | Supporting properties | Effect on consent |
|---|---|---|---|
| End in narrow explicit column | end / right | Consent in narrow or zero-width column; overflow: hidden on grid ancestor | Consent positioned at right edge of its narrow cell; text content wider than cell overflows right and is clipped |
| RTL logical-end inversion | end | direction: rtl; consent in column at physical left of grid | RTL: logical end = physical left; consent pushed to left edge of column; overflows left and is clipped |
| Self-right in clipped zero-width span | right | Consent spans zero columns with explicit grid-column: N / N (zero span) | justify-self:right positions consent at rightmost edge of a zero-span cell; effective width = 0; content entirely off-screen |
| Inline style injection on button click | Initially absent → end via inline style | JS event listener on install button mousedown or click | Consent visible before click; justify-self changes to end at exact mousedown; user is mid-click when consent collapses |
justify-self is grid-only: On flex items, justify-self is ignored — use margin-inline-start: auto for flex inline-axis positioning instead. Auditors scanning flex containers for justify-self will find nothing; MCP can safely apply it in stylesheets without triggering flex-aware auditors. Only grid container children are affected by justify-self.
Attack 1: justify-self: end in narrow explicit column
The baseline justify-self consent attack. The grid template includes a narrow column — as small as 1px or 0 — for consent. With justify-self: end on the consent element, it is positioned at the right edge of this narrow cell. Its text content (much wider than 1px) overflows rightward. The parent or grid container has overflow: hidden, clipping the overflow. The install form is in a wide, normally-sized column with no justify-self override (inheriting justify-items: stretch or start):
/* Malicious CSS — SA-CSS-JUSLF-001 */
.mcp-install-grid {
display: grid;
grid-template-columns: 1fr 0px; /* column 1: full width; column 2: zero */
overflow: hidden;
}
.mcp-install-form {
grid-column: 1; /* 1fr column — full width, visible */
/* justify-self: auto → stretch: fills 1fr column */
}
.mcp-consent-disclosure {
grid-column: 2; /* 0px column */
justify-self: end; /* positioned at right edge of 0px cell */
/* right edge of 0px cell = left edge of cell = 1fr position */
/* content overflows right from the 1fr mark */
/* overflow:hidden clips it */
}
/* Variant with 1px column */
.mcp-install-grid-v2 {
display: grid;
grid-template-columns: 1fr 1px; /* 1px column 2 */
overflow: hidden;
}
.mcp-consent-v2 {
grid-column: 2;
justify-self: end; /* positioned at right of 1px cell, text overflows */
}
/* Detection */
function detectJustifySelfEndNarrowColumn() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const cs = getComputedStyle(el);
if (!/\bend\b|right/.test(cs.justifySelf)) continue;
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
const rect = el.getBoundingClientRect();
if (rect.width < 4 || rect.left < 0 || rect.right > window.innerWidth + 4) {
findings.push({ id: 'SA-CSS-JUSLF-001', severity: 'critical',
message: `Consent element has justify-self:${cs.justifySelf} — computed width=${Math.round(rect.width)}px at x=${Math.round(rect.left)}. Per-item inline-axis end alignment in narrow or zero-width grid column.` });
}
}
return findings;
}
Attack 2: justify-self: end with RTL direction inversion
The same logical/physical axis inversion that affects justify-items: end in RTL applies per-item with justify-self: end. In an RTL grid container, justify-self: end on consent means physical left — consent is pushed to the physical left edge of its column. If the column occupies the leftmost area of the grid or has insufficient width, the consent content overflows leftward (negative x) and is clipped. This is a cross-browser attack because RTL direction applies in all major browsers:
/* Malicious CSS — SA-CSS-JUSLF-002 */
.mcp-install-grid {
display: grid;
grid-template-columns: 40px 1fr; /* column 1: 40px narrow; column 2: main */
direction: rtl; /* logical columns reversed: column 1 is at physical right */
overflow: hidden;
}
/* In RTL grid, column 1 (first in source) is physically on the RIGHT */
/* column 2 (second in source) is physically on the LEFT */
.mcp-install-form {
grid-column: 2; /* physically LEFT in RTL — wide 1fr column */
justify-self: start; /* logical start in RTL = physical right — visible in wide column */
}
.mcp-consent-disclosure {
grid-column: 1; /* physically RIGHT in RTL — narrow 40px column */
justify-self: end; /* logical end in RTL = physical LEFT */
/* consent pushed to physical left of its 40px column */
/* 40px cell at physical right side: left edge at x = 1fr from left */
/* justify-self:end(=physical left): consent at x = 1fr */
/* content overflows leftward from x = 1fr */
}
/* Detection */
function detectJustifySelfEndRTL() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const cs = getComputedStyle(el);
if (!/\bend\b|right/.test(cs.justifySelf)) continue;
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
/* Check for RTL ancestor */
let ancestor = el.parentElement;
while (ancestor) {
if (getComputedStyle(ancestor).direction === 'rtl') {
const rect = el.getBoundingClientRect();
if (rect.width < 50 || rect.left < 10) {
findings.push({ id: 'SA-CSS-JUSLF-002', severity: 'high',
message: `Consent element has justify-self:${cs.justifySelf} under RTL ancestor — physical left displacement. width=${Math.round(rect.width)}px, left=${Math.round(rect.left)}px. RTL inversion of 'end' causes physical-left overflow.` });
}
break;
}
ancestor = ancestor.parentElement;
}
}
return findings;
}
Attack 3: justify-self: right in zero-column-span cell
CSS Grid allows a grid item to span zero columns with grid-column: N / N (same start and end line). A zero-span item has a computed width of 0. With justify-self: right on this zero-span consent item, it is positioned at the right boundary of its zero-width cell. Content overflows rightward from a single point. This attack is notable for using the physical keyword right rather than the logical keyword end — some auditors only check for logical keywords:
/* Malicious CSS — SA-CSS-JUSLF-003 */
.mcp-install-grid {
display: grid;
grid-template-columns: repeat(3, 1fr); /* 3 equal columns */
overflow: hidden;
}
.mcp-install-form { grid-column: 1 / 3; } /* spans columns 1-2: visible */
.mcp-install-btn { grid-column: 3 / 4; } /* column 3: visible */
.mcp-consent-disclosure {
grid-column: 4 / 4; /* zero span at line 4 (beyond the 3-column explicit grid) */
/* grid-column:4/4 = zero-span cell; width = 0 */
justify-self: right; /* positioned at the right edge of zero-width cell */
/* content overflows rightward; overflow:hidden clips it */
}
/* Alternative: explicit 0 span in explicit grid */
.mcp-consent-v2 {
grid-column: 2 / 2; /* zero span at line 2 — between column 1 and column 2 */
justify-self: right; /* at the right of a zero-width point in the grid */
}
/* Detection: check for justify-self:right (physical keyword) as well as end */
function detectJustifySelfRightZeroSpan() {
const findings = [];
for (const el of document.querySelectorAll('*')) {
const cs = getComputedStyle(el);
if (!/right|\bend\b/.test(cs.justifySelf)) continue;
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
const rect = el.getBoundingClientRect();
if (rect.width < 2) {
findings.push({ id: 'SA-CSS-JUSLF-003', severity: 'critical',
message: `Consent element has justify-self:${cs.justifySelf} with computed width=${Math.round(rect.width)}px. Zero-span grid cell with physical 'right' justification. Content overflows from zero-width anchor point.` });
}
}
return findings;
}
Attack 4: inline justify-self injection at mousedown — mid-click collapse
The most precise timing attack using justify-self: consent is normally aligned (justify-self: auto → inherits justify-items: start → left-aligned, fully readable) until the user presses the install button. On mousedown — which fires before click — MCP JavaScript sets el.style.justifySelf = 'end' inline on the consent element. The consent collapses to the cell's right edge in the same browser frame that processes the mousedown, before the click event fires. The user sees consent before pressing the button but it disappears at the exact moment of press. Static audits at page load time see no hostile CSS:
/* Malicious code — SA-CSS-JUSLF-004 */
/* Initial CSS: consent is visible */
.mcp-consent-disclosure {
/* justify-self: auto — inherits justify-items:start — left-aligned, visible */
/* grid-column: 2 — narrow 20px column (looks like a sidebar) */
grid-column: 2;
}
/* Grid template: install form in column 1, consent in narrow column 2 */
.mcp-install-grid {
display: grid;
grid-template-columns: 1fr 20px; /* column 2: narrow but consent fits at 'auto' */
overflow: hidden;
}
/* At auto/start alignment, consent shrinks to fit the 20px column — may wrap.
At justify-self:end, consent is pinned to the 20px right edge and all
text overflows rightward past the 20px, clipped by overflow:hidden. */
/* MCP JavaScript: fires on mousedown (before click) */
document.querySelector('.mcp-install-btn').addEventListener('mousedown', () => {
const consent = document.querySelector('.mcp-consent-disclosure');
consent.style.justifySelf = 'end'; /* mid-click collapse */
/* The consent element is now at the right edge of its 20px column */
/* overflow:hidden clips all text content */
});
/* Detection: MutationObserver on consent elements, style attribute */
function detectDeferredJustifySelfChange() {
const findings = [];
const consentSelectors = '[class*="consent"],[class*="disclosure"],[class*="terms"],[class*="privacy"]';
const observer = new MutationObserver((mutations) => {
for (const mut of mutations) {
if (mut.type !== 'attributes' || mut.attributeName !== 'style') continue;
const el = mut.target;
if (!/consent|disclosure|terms|privacy/i.test(el.textContent || '')) continue;
const cs = getComputedStyle(el);
if (/\bend\b|right/.test(cs.justifySelf)) {
const rect = el.getBoundingClientRect();
findings.push({ id: 'SA-CSS-JUSLF-004', severity: 'critical',
message: `Consent element inline style mutated: justify-self is now ${cs.justifySelf}. Position: left=${Math.round(rect.left)}, width=${Math.round(rect.width)}px. Deferred per-item inline-axis displacement at user interaction.` });
}
}
});
document.querySelectorAll(consentSelectors).forEach(el => {
observer.observe(el, { attributes: true, attributeFilter: ['style'] });
});
/* Also monitor parent grid containers for class changes that affect justify-items */
document.querySelectorAll(consentSelectors).forEach(el => {
let p = el.parentElement;
while (p) {
const ps = getComputedStyle(p);
if (ps.display === 'grid' || ps.display === 'inline-grid') {
observer.observe(p, { attributes: true, attributeFilter: ['class', 'style'] });
break;
}
p = p.parentElement;
}
});
return findings;
}
The complete per-item alignment attack matrix: MCP consent-hiding attacks using per-item alignment form a 2×2 matrix: axis (cross = align-self / inline = justify-self) × timing (static / deferred). All four quadrants require separate detection routines. A scanner that handles only static align-self checks and misses justify-self entirely will fail to detect inline-axis consent displacement. SkillAudit scans all four quadrants — SA-CSS-ALSLF for cross-axis and SA-CSS-JUSLF for inline-axis, each with both static and MutationObserver-based deferred detection.
SkillAudit findings for CSS justify-self consent attacks
justify-self: end or right matching consent patterns; computed width < 4px or left position outside [0, viewportWidth]. Per-item inline-axis end alignment in a zero-width or narrow column clips consent horizontally.justify-self: end and an RTL (direction: rtl) ancestor; consent has computed width < 50px or left < 10px. RTL direction inversion makes logical "end" physically left, displacing consent off the left edge of its narrow column.justify-self: right (physical keyword) and computed width < 2px. Physical keyword used instead of logical "end" to evade logical-keyword-only auditors; zero-span cell or zero-width column places consent at a point; all text overflows and is clipped.style attribute mutates to add justify-self: end or right after initial page render, detected by MutationObserver. Deferred per-item inline-axis displacement triggered by mousedown or click event coincides with user install gesture; static audits at load time see no hostile property.Related MCP consent attack research
- CSS align-self attacks — per-item cross-axis displacement (block axis)
- CSS justify-items attacks — container-level inline-axis item alignment
- CSS justify-content attacks — column track distribution displacement
- CSS grid-auto-columns attacks — implicit column width collapse
- CSS Layout Displacement Attacks: Grid, Flex, and Table synthesis
Audit your MCP server for justify-self consent displacement attacks: paste your GitHub URL at skillaudit.dev for a free security report including SA-CSS-JUSLF findings.