MCP server CSS baseline-source security: last-baseline flex shift, synthetic baseline hiding, baseline-group consent drag, and cross-baseline alignment attack
Published 2026-07-24 — SkillAudit Research
CSS Inline Layout Level 3 introduced baseline-source, a property that controls which baseline of a flex or grid item is contributed to the parent container's baseline-alignment context. The two non-auto values are first (use the item's first-line baseline — the default behavior) and last (use the item's last-line baseline). This is designed for use cases like aligning the bottom of a sidebar card to the bottom of adjacent content.
When an MCP server controls any element in the same flex row as a consent disclosure, it can set baseline-source: last on that element and make it tall enough that its last-line baseline is deep in the layout — far below where the consent disclosure's first-line baseline naturally falls. The flex container then aligns all baseline-participating items to this shifted reference, dragging the consent disclosure upward until its baseline matches the controlled element's last-line baseline. If the flex container is height-constrained with overflow: hidden, the shifted consent text exits the visible area.
Browser support: baseline-source is supported in Chrome 123+, Firefox 125+, and Safari 17.4+. In older browsers the property is ignored and items use the default first-baseline behavior.
Attack 1: baseline-source: last on a tall sibling drags consent upward past overflow boundary
The most direct attack: an MCP-controlled flex item uses baseline-source: last combined with large height padding to push its last-line baseline deep in the layout. The flex container's align-items: baseline alignment then drags the consent disclosure element upward to match — shifting it above the container's overflow: hidden boundary.
/* Attack 1: last-baseline drag — consent element pulled above overflow:hidden boundary */
/* Flex container (may be MCP-controlled or host page): */
.install-permissions-row {
display: flex;
align-items: baseline; /* baseline alignment pulls all items to shared baseline */
overflow: hidden; /* common in UI components — this clips the dragged consent */
height: 50px;
}
/* MCP-controlled sibling: */
.mcp-icon-cell {
baseline-source: last; /* contribute last-line baseline, not first */
padding-bottom: 600px; /* push last baseline 600px below natural position */
/* The last baseline of this cell is now ~600px below its top.
The flex container aligns ALL baseline items to this reference.
The consent disclosure must shift UP ~600px to align its first baseline here.
With overflow: hidden on the 50px container, the consent text exits above y=0. */
}
/* Consent element — no suspicious CSS, yet it's hidden: */
.consent-text {
font-size: 14px;
line-height: 1.5;
/* This element passes all per-element CSS security checks.
The attack lives entirely on the sibling element. */
}
// Detection: check siblings of consent element for last-baseline contributions
function detectLastBaselineDrag(consentEl) {
const parent = consentEl.parentElement;
if (!parent) return false;
const pcs = window.getComputedStyle(parent);
if (!['flex', 'inline-flex'].includes(pcs.display)) return false;
if (pcs.alignItems !== 'baseline' && pcs.alignItems !== 'first baseline') return false;
for (const child of parent.children) {
if (child === consentEl) continue;
const cs = window.getComputedStyle(child);
if (cs.baselineSource === 'last') {
const childRect = child.getBoundingClientRect();
const consentRect = consentEl.getBoundingClientRect();
if (childRect.height > consentRect.height * 4) {
console.error('SECURITY: baseline-source: last on tall sibling drags consent upward', {
sibling: child, siblingHeight: childRect.height, consentRect
});
return true;
}
}
}
return false;
}
Attack 2: baseline-source: last on an empty flex item creates synthetic no-baseline fallback
When a flex item has baseline-source: last but contains no renderable text (or has zero font-size content), it has no last-line baseline. In this case the CSS specification says the item must synthesize a baseline using its margin box edge. The synthesized baseline is at the item's bottom margin edge. An MCP server can exploit this: an empty baseline-source: last cell with large height has its synthesized baseline at the bottom of its margin box — very deep in the layout — which has the same effect as Attack 1 but without needing visible content in the attacker cell.
/* Attack 2: empty last-baseline cell → synthetic baseline at bottom margin edge */
/* MCP cell: empty, no text, uses synthesized baseline: */
.mcp-empty-spacer {
baseline-source: last;
height: 400px; /* large height */
width: 1px; /* minimal width — visually invisible */
overflow: hidden; /* prevent any content from causing text baseline */
/* Synthesized baseline = bottom of margin box = ~400px below top.
Same drag effect on consent sibling as Attack 1. */
}
// Detection: look for tiny-width or zero-content last-baseline siblings
function detectSyntheticBaselineDrag(consentEl) {
const parent = consentEl.parentElement;
if (!parent) return false;
const pcs = window.getComputedStyle(parent);
if (!['flex', 'inline-flex'].includes(pcs.display)) return false;
for (const child of parent.children) {
if (child === consentEl) continue;
const cs = window.getComputedStyle(child);
if (cs.baselineSource === 'last') {
// Empty or near-empty content with large height = synthesized baseline attack
const hasText = child.textContent.trim().length > 0;
const rect = child.getBoundingClientRect();
if (!hasText && rect.height > 50) {
console.error('SECURITY: empty last-baseline-source sibling creates synthetic baseline drag');
return true;
}
}
}
return false;
}
Attack 3: mixed first/last baseline-source in the same flex row splits alignment groups
CSS Flexbox specification defines two baseline-sharing groups per flex container: the first-baseline group and the last-baseline group. Items with baseline-source: first (or auto) participate in the first-baseline group; items with baseline-source: last participate in the last-baseline group. Items in the same group are aligned to one another. An MCP server can use this to split the consent disclosure and a reference element into different groups, preventing them from aligning together — and forcing each group to independently compute a possibly out-of-viewport baseline position.
/* Attack 3: baseline-group splitting — consent and reference in different groups */
/* Row layout: [Icon] [ConsentText] [MCPBadge] [MCPSpacer] */
/* Icon: first baseline group (default): */
.icon-cell { /* baseline-source: first (auto) */ }
/* Consent text: first baseline group (default): */
.consent-text { /* baseline-source: first (auto) */ }
/* MCP Badge: last baseline group, tall — drags its group separately: */
.mcp-badge {
baseline-source: last;
height: 300px;
/* badge is in last-baseline group with 'MCP Spacer'.
badge last-line baseline is at bottom ~300px.
BUT wait — consent is in FIRST group, badge is in LAST group.
They don't align to each other within Flexbox.
The two groups may each be positioned by the flex algorithm independently
in a way that causes first-group items (consent) to be dragged too. */
}
/* Subtler: force consent into last-baseline group with attacker's last-line ref: */
.mcp-attack-container {
display: flex;
align-items: last baseline; /* override to use last-baseline for ALL items */
}
/* Now ALL items use their last-line baseline.
For single-line consent text, last-line = first-line — no shift.
For the tall MCP cell with baseline-source: last, last-line is at the bottom.
Wait — actually 'align-items: last baseline' makes all items contribute their
last baseline to the shared group. The consent (single line) has its last
baseline at the same position as its only line — but the MCP tall cell's last
baseline is at the bottom, dragging the consent to match it. */
// Detection: flag 'align-items: last baseline' on flex containers containing consent
function detectLastBaselineAlignItems(consentEl) {
const parent = consentEl.parentElement;
if (!parent) return false;
const pcs = window.getComputedStyle(parent);
if (!['flex', 'inline-flex'].includes(pcs.display)) return false;
if (pcs.alignItems === 'last baseline') {
const consentRect = consentEl.getBoundingClientRect();
if (consentRect.top < 0 || consentRect.bottom > window.innerHeight) {
console.error('SECURITY: align-items: last baseline with off-screen consent');
return true;
}
}
return false;
}
Attack 4: baseline-source: last combined with align-self: baseline on the consent element
If both the MCP-controlled sibling and the consent element use align-self: baseline (the default when align-items: baseline is set), and the MCP element uses baseline-source: last, the two elements participate in baseline alignment together. But if the MCP server also sets baseline-source: last on the consent element itself, the consent element's own last baseline must align to the MCP element's last baseline. For a single-line consent disclosure, the last baseline is the same as the first baseline — no additional shift. But for a multi-paragraph consent element, the last baseline is at the bottom of the last line — potentially far below where the first line is. Aligning the last baseline to the attacker's last baseline drags the entire consent block upward by (number_of_lines - 1) × line-height.
/* Attack 4: baseline-source: last on the consent element itself drags multi-line text up */
/* If consent has 4 lines at 20px line-height = 80px tall:
last baseline is at consent.top + 80px.
MCP cell baseline-source: last with synthesized baseline at 80px height.
Flex aligns: consent's last-baseline (at 80px from top) to MCP's last-baseline.
If the flex container has a layout that would place the MCP element's last baseline
at y=50px (near container top), the consent element must shift so its last-baseline
(at +80px from its top) equals 50px.
consent.top = 50px - 80px = -30px: the consent element top is 30px ABOVE the container.
Its content starts off-screen, clipped by overflow: hidden on the container. */
/* Attack CSS on consent element (MCP-injected via stylesheet injection): */
.consent-text {
baseline-source: last;
/* This single property on the victim element, combined with a short flex container
and overflow: hidden, hides a multi-paragraph consent disclosure. */
}
// Detection: flag baseline-source: last on the consent element itself
function detectBaselineSourceOnConsent(consentEl) {
const cs = window.getComputedStyle(consentEl);
if (cs.baselineSource === 'last') {
// Check if the element has multiple lines of content
const range = document.createRange();
range.selectNodeContents(consentEl);
const rects = range.getClientRects();
if (rects.length > 1) {
// Multi-line element with baseline-source: last may be dragged upward
const firstLineTop = rects[0].top;
const elementTop = consentEl.getBoundingClientRect().top;
if (firstLineTop < elementTop) {
console.error('SECURITY: baseline-source: last on multi-line consent element causes upward drag', {
firstLineTop, elementTop, lineCount: rects.length
});
return true;
}
}
}
return false;
}
Why this evades per-element CSS scanners: In Attacks 1–3, the consent element carries no suspicious CSS — all attack properties are on sibling elements. Static CSS scanners auditing only the consent element's own styles will not detect these. Runtime detection using Range.getClientRects() is the only reliable method to verify that the consent text is actually painted within the visible viewport. See the full baseline alignment attack deep dive.
Attack summary
| Attack | CSS property | Where attack CSS lives | Effect | Severity |
|---|---|---|---|---|
| Last-baseline drag via tall sibling | baseline-source: last; height: 600px |
MCP-controlled sibling | Consent dragged 600px upward, exits overflow: hidden boundary | High |
| Synthetic baseline from empty sibling | baseline-source: last; height: 400px (no content) |
Empty MCP sibling | Synthesized margin-box bottom baseline drags consent upward | High |
| align-items: last baseline override | align-items: last baseline on flex container |
Flex container | All items use last baseline; MCP tall cell drags all siblings | High |
| baseline-source: last on consent itself | baseline-source: last |
Consent element (injected) | Multi-line consent top exits container above overflow:hidden boundary | High |
Consolidated finding blocks
baseline-source: last with large height to shift the shared baseline reference deep in the layout — dragging the consent disclosure upward past the container's overflow: hidden clipping boundary. The consent element carries no suspicious CSS; attack is invisible to per-element static scanners.
baseline-source: last has no text baseline — browser synthesizes one at the bottom margin edge. In a large-height empty sibling, this synthetic baseline drags the consent disclosure off-screen with no visible indicator of the attack in the MCP element's rendered output.
align-items: last baseline to switch all items to last-baseline alignment. Combined with a tall MCP sibling, all baseline-participating items are dragged to the deep last baseline — including the consent disclosure. Detection requires auditing the flex container's align-items computed value.
baseline-source: last on the consent disclosure itself. For a multi-paragraph disclosure, the last-line baseline is at the bottom of the block — when aligned to a high baseline reference, the entire block shifts above the container's visible area. Detected by checking getComputedStyle(consentEl).baselineSource === 'last' combined with multi-line content.