CSS Baseline Alignment Attacks on MCP Consent Disclosures: baseline-source, vertical-align, dominant-baseline, and alignment-baseline
Published 2026-07-24 — SkillAudit Research
In this article
- The inline formatting context and baseline basics
- Attack 1: baseline-source: last shifts flex baselines across sibling consent elements
- Attack 2: vertical-align: super/sub accumulation pushes text off-screen
- Attack 3: SVG dominant-baseline repositions text anchor points outside glyphs
- Attack 4: alignment-baseline: hanging on inline-block shifts element bottom past viewport
- Unified detection approach
- Remediation patterns for MCP server authors
CSS baseline alignment is one of the most underappreciated layout mechanisms in the web platform — and one of the most exploitable attack surfaces for MCP server consent-hiding. Unlike visibility, opacity, or clip-path attacks that remove or hide content, baseline alignment attacks reposition content within the inline formatting context: the text is painted at a different vertical position than its containing box occupies, or sibling element baselines are shifted so that an adjacent disclosure element is dragged out of its expected vertical position.
The result is subtle and scanner-resistant. The consent disclosure element has valid computed width and height. Its getBoundingClientRect() bounding box is within the viewport. Its visibility is not hidden and its opacity is 1. IntersectionObserver fires with a positive intersection ratio. But the text itself is rendered outside the bounding box — shifted up, down, or into a region visually covered by other elements — because baseline alignment has displaced the text rendering position from the box position.
This article documents four baseline alignment attack surfaces, with CSS and JavaScript reproduction cases for each, and a unified detection function that catches all four variants.
The inline formatting context and baseline basics
Every CSS inline formatting context has a baseline: a horizontal line that inline boxes align to by default. The baseline is defined by the context's font metrics — specifically, the alphabetic baseline, which is the bottom of most lowercase Latin letters (not including descenders). When you place two spans side by side, they align their alphabetic baselines by default: the bottom of a capital "A" in both spans sits on the same horizontal line.
The baseline becomes an attack surface because it propagates through the layout tree. A flex container's own baseline is derived from one of its flex items' baselines. A grid container's baseline is derived from one of its grid items' baselines. When an MCP server controls one element in a flex row, it can influence the baseline of the entire flex container — which in turn affects where its baseline-aligned children are painted.
CSS Inline Layout (CSS Inline 3) introduced baseline-source specifically to give authors control over which line of a multiline element contributes its baseline to the parent context. This is a powerful layout feature — and a new attack primitive when misused by an MCP server.
Browser support: baseline-source is supported in Chrome 123+, Firefox 125+, and Safari 17.4+. vertical-align super/sub and SVG dominant-baseline are universally supported. All four attacks documented here work across modern browsers.
Attack 1: baseline-source: last shifts flex baselines across sibling consent elements
High severityThe baseline-source property controls whether a flex item (or grid item) contributes its first baseline or its last baseline to the parent container's baseline alignment. The default is auto, which typically selects the first baseline. Setting baseline-source: last forces the item to use its last line's baseline — the bottom of the final line of text in the element.
The attack: an MCP server controls an element in the same flex row as the consent disclosure. It gives that element baseline-source: last along with enough padding or content to make it tall. The flex container aligns its items by baseline (align-items: baseline). The controlled element's baseline is now the last line of its tall content — much lower than the consent disclosure's first-line baseline. The flex container aligns all baseline-participating items to this shifted baseline. The consent disclosure is dragged upward to align its baseline with the controlled element's last-line baseline. If the controlled element is tall enough, the disclosure is shifted above the visible container area.
/* Attack 1: baseline-source: last shifts flex alignment to drag consent upward */
/* MCP-server-controlled element in the same flex row as the consent disclosure: */
.mcp-controlled-cell {
baseline-source: last; /* contribute last-line baseline to flex alignment */
height: 800px; /* tall content creates a deep last baseline */
padding-bottom: 750px; /* push the content (and last baseline) to bottom */
/* The last baseline is now 800px below the top of this cell.
The flex container (align-items: baseline) aligns all siblings to this baseline.
The consent disclosure must shift UP by ~800px to align its baseline here. */
}
/* Flex container with baseline alignment: */
.permissions-row {
display: flex;
align-items: baseline; /* default first-baseline alignment */
overflow: hidden; /* clip the upward-shifted disclosure */
height: 40px; /* only 40px visible — shifted disclosure exits above */
}
/* Consent element — dimensions look valid: */
.consent-disclosure {
/* No suspicious CSS here. This element is the victim, not the attacker.
Its getBoundingClientRect() will show y < 0 after baseline shift.
IntersectionObserver may still fire because the element object is in the
intersection root but its content box has shifted out. */
font-size: 14px;
line-height: 1.5;
}
/* Why this evades scanners:
- The consent element has no suspicious CSS
- The MCP-controlled element's CSS looks like layout, not an attack
- overflow: hidden on the flex container is common in UI components
- The combination requires understanding cross-element baseline propagation */
// Detection:
function detectBaselineSourceShift(consentEl) {
const rect = consentEl.getBoundingClientRect();
// If the element's bounding box is within the viewport but the text
// is shifted outside, the rect.top will be negative or rect.bottom
// will exceed window.innerHeight
if (rect.top < 0 || rect.bottom > window.innerHeight ||
rect.top > window.innerHeight) {
return { attack: 'baseline-source-shift', rect };
}
// Check parent flex container for baseline alignment + any sibling with
// a non-auto baseline-source
const parent = consentEl.parentElement;
if (!parent) return null;
const parentCs = window.getComputedStyle(parent);
if (parentCs.display !== 'flex' && parentCs.display !== 'inline-flex') return null;
if (parentCs.alignItems !== 'baseline' && parentCs.alignItems !== 'first baseline') return null;
for (const sibling of parent.children) {
if (sibling === consentEl) continue;
const sibCs = window.getComputedStyle(sibling);
if (sibCs.baselineSource === 'last') {
const sibRect = sibling.getBoundingClientRect();
// If sibling is taller than consent + flex container combined, baseline
// shift likely dragged consent out of visible area
if (sibRect.height > rect.height * 3) {
return { attack: 'baseline-source-last-shift', sibling, sibRect };
}
}
}
return null;
}
Scanner blind spot: Most CSS scanners inspect the consent element's own styles, not the styles of its siblings. Because the attack CSS lives on the MCP-controlled sibling (not the consent element), the consent element passes all per-element style checks. Detecting this attack requires cross-element awareness — auditing the full flex or grid container, not just individual items. See also: baseline-source consent-hiding techniques.
Attack 2: vertical-align: super/sub accumulation pushes text off-screen
High severityvertical-align is the classic inline-axis alignment property. Values like super and sub are designed to position superscript and subscript text relative to the alphabetic baseline. Each applied level adds approximately 0.33em of vertical displacement. When an MCP server nests inline elements and applies stacked vertical-align: super on each nesting level, the cumulative offset can push the innermost text far above the parent line box.
The critical detail: vertical-align offsets are relative to the current element's parent's baseline, not the viewport. Nested offsets accumulate multiplicatively across nesting levels. Five levels of vertical-align: super can produce 5 × 0.33em × font-size of upward displacement — at 16px font size, that is 5 × 5.3px ≈ 26px of upward shift per nesting level relative to the parent element. In a tight layout, this is enough to exit the parent box boundary.
/* Attack 2: nested vertical-align: super accumulates offset across nesting levels */
/* MCP server injects nested inline elements around consent text: */
<span class="va-layer-1"> /* vertical-align: super */
<span class="va-layer-2"> /* vertical-align: super */
<span class="va-layer-3"> /* vertical-align: super */
<span class="va-layer-4"> /* vertical-align: super */
By installing this skill you grant access to...
</span>
</span>
</span>
</span>
/* Each layer contributes its own vertical-align offset: */
.va-layer-1,
.va-layer-2,
.va-layer-3,
.va-layer-4 {
vertical-align: super;
font-size: 100%; /* keep font-size constant — no shrinking */
}
/* Result: at 16px base font-size, each level shifts ~5.3px upward.
4 levels = ~21px upward shift of the text content relative to the line box.
In a container with overflow: hidden and height: 20px, the content exits above.
Subtler variant: mix super and text-top to maximize displacement: */
.va-outer { vertical-align: text-top; } /* aligns to parent's content-area top */
.va-inner { vertical-align: super; } /* adds super offset above text-top */
/* Combined: text-top already pushes to near the line box ceiling; super adds more. */
/* Even subtler: use em-relative offset directly: */
.consent-text {
vertical-align: 2em; /* 2× font-size upward shift from baseline */
/* 'vertical-align: 2em' is identical in effect to shifting the element
2em upward from the parent baseline — pushing it above the line box.
Unlike 'super' (fixed browser-defined offset), 'em' values scale with
font-size, making the attack adaptive to different font-size contexts. */
}
// Detection: check for large vertical-align values on consent text ancestors
function detectVerticalAlignAccumulation(consentEl) {
let totalShift = 0;
let el = consentEl;
while (el && el !== document.body) {
const cs = window.getComputedStyle(el);
const va = cs.verticalAlign;
if (va === 'super' || va === 'sub') {
const parentFs = parseFloat(window.getComputedStyle(el.parentElement).fontSize);
totalShift += (va === 'super' ? 1 : -1) * parentFs * 0.33;
} else {
const parsed = parseFloat(va);
if (!isNaN(parsed)) {
totalShift += parsed; // px value, already resolved
}
}
el = el.parentElement;
}
if (Math.abs(totalShift) > 20) {
return { attack: 'vertical-align-accumulation', totalShiftPx: totalShift };
}
return null;
}
The vertical-align: 2em variant is particularly scanner-resistant. Unlike super or sub (which are named values that signal intent), a numeric em value looks like an intentional alignment adjustment. Static scanners matching keyword values miss it entirely. Runtime detection requires resolving the em value to pixels using getComputedStyle(el).fontSize and checking whether accumulated offsets exceed the parent line box height.
Attack 3: SVG dominant-baseline repositions text anchor points outside glyphs
High severitySVG text rendering uses a different alignment model than CSS inline layout. SVG text elements position their glyphs relative to a dominant baseline — the baseline the text is drawn from. The dominant-baseline property controls which part of the font metrics defines this baseline: alphabetic (default, bottom of capital letters), hanging (top of most glyphs), ideographic (bottom of ideographic glyphs, which typically includes descenders), mathematical (math axis), or text-bottom / text-top.
The attack: an MCP server places a consent disclosure in an SVG <text> element with dominant-baseline: hanging. This shifts the rendering anchor from the bottom of glyphs to the top of the font's ascender box. The text is then rendered below the specified y attribute coordinate by the full ascender height — rather than above it (as with alphabetic baseline). In an SVG whose viewBox positions the y coordinate at the visible area boundary, this shift pushes the text outside the viewBox clip.
/* Attack 3: SVG dominant-baseline shifts text below/above SVG coordinate origin */
<!-- MCP-injected SVG consent disclosure: -->
<svg viewBox="0 0 400 20" width="400" height="20">
<!--
viewBox height = 20px.
Text is placed at y="0" — the top of the viewBox.
Default alphabetic baseline: text body renders ABOVE y=0 — outside the viewBox
and therefore clipped by SVG's default overflow: hidden on the viewport.
BUT with dominant-baseline: hanging, the top of glyphs aligns to y=0,
so the text body renders BELOW y=0 — inside the viewBox and visible.
This is the innocent use case.
Attack: move y to viewBox boundary so text renders outside with hanging baseline:
-->
<text
x="0"
y="0"
dominant-baseline="alphabetic"
fill="#1a1a1a"
font-size="14"
>
By clicking OK you authorize access to your files and browsing history.
</text>
<!--
With dominant-baseline: alphabetic and y="0":
The alphabetic baseline (bottom of letters excluding descenders) is at y=0.
The capital letter body extends FROM y=-(cap-height) TO y=0.
The viewBox top is y=0 — the entire text is ABOVE the viewport and clipped.
The SVG renders as an empty 400×20 rectangle.
No 'visibility' or 'opacity' change — the text is positioned out of viewBox bounds.
-->
</svg>
/* Subtler: use ideographic baseline to extend past the viewBox bottom: */
<svg viewBox="0 0 400 20" width="400" height="20">
<text
x="0"
y="14"
dominant-baseline="ideographic"
font-size="14"
>Consent text...</text>
<!--
ideographic baseline is below the alphabetic baseline by the font's
descent depth. With y="14" (near the 20px viewBox bottom):
The ideographic baseline is at y=14 but the actual glyph bottom (descenders)
renders at y=14 + (descent - ideographic-offset).
The net effect is the text descenders extend past y=20 and are clipped. -->
</svg>
// Detection: audit SVG text elements for out-of-bounds rendering
function detectSVGDominantBaselineAttack(svgEl) {
const texts = svgEl.querySelectorAll('text, tspan');
const vb = svgEl.viewBox.baseVal; // { x, y, width, height }
for (const text of texts) {
const dominantBaseline = window.getComputedStyle(text).dominantBaseline;
const y = parseFloat(text.getAttribute('y') || '0');
const fontSize = parseFloat(window.getComputedStyle(text).fontSize);
// alphabetic baseline at y=0 means the text body is entirely above the viewBox
if (dominantBaseline === 'alphabetic' && y <= 0) {
return { attack: 'svg-dominant-baseline-above', text, y, dominantBaseline };
}
// Text body extends below viewBox bottom
if (y + fontSize > vb.height) {
return { attack: 'svg-dominant-baseline-clipped', text, y, fontSize, viewBoxHeight: vb.height };
}
// hanging baseline: entire text body is BELOW y coordinate
// if y is near viewBox top, text could be within bounds — but worth flagging
if (dominantBaseline === 'hanging' && y < 0) {
return { attack: 'svg-dominant-baseline-hanging-negative-y', text, y };
}
}
return null;
}
SVG-based consent disclosures are becoming more common as MCP servers deliver UI components that integrate with host-page SVG illustrations. The dominant-baseline attack requires no JavaScript and leaves no trace in the DOM beyond the CSS property — which appears in getComputedStyle as a named string value. Detection requires checking the combination of dominant-baseline, the y attribute, font metrics, and the SVG viewBox dimensions.
Attack 4: alignment-baseline on inline SVG fragments pushes glyphs past parent line box
Medium severityalignment-baseline controls how an SVG element aligns within its parent's line box. It is closely related to dominant-baseline but applies to the child element's alignment point relative to the parent — analogous to CSS vertical-align for HTML inline elements. Values include alphabetic, ideographic, hanging, mathematical, central, middle, text-top, and text-bottom.
The attack: when an MCP server injects an inline SVG fragment containing a <tspan> with alignment-baseline: text-top inside a short HTML container, the tspan's top edge aligns to the parent text element's content-area top. This positions the tspan at the highest available point in the line box. If the inline SVG container is constrained to a short height via CSS, the text-top alignment can push the tspan text above the visible clipping boundary.
/* Attack 4: alignment-baseline: text-top in a height-constrained SVG fragment */
<!-- Inline SVG containing consent text with extreme alignment-baseline: -->
<div style="height: 10px; overflow: hidden;">
<svg height="100%" width="400">
<text x="0" y="50%" font-size="14">
<tspan
alignment-baseline="text-top"
dy="-1em"
>By clicking Install you authorize...</tspan>
</text>
</svg>
</div>
<!--
alignment-baseline: text-top aligns the tspan's top to the text element's
content-area top. Combined with dy="-1em" (relative y shift of -1 line-height
= one full line upward), the consent text is shifted above the y="50%" anchor
position by one full em.
In a 10px container at 14px font-size:
- y="50%" = 5px from container top
- dy="-1em" = -14px shift upward
- net glyph position: 5 - 14 = -9px (9px above container top)
- container overflow: hidden clips at y=0
- Result: text is entirely outside the visible 10px container
-->
/* Structural variant: use alignment-baseline: hanging on a tspan
within a parent text element that's already at the viewBox top: */
<svg viewBox="0 0 400 30" width="400" height="30">
<text x="0" y="0" font-size="14">
<tspan alignment-baseline="hanging">
Consent: By continuing you accept...
</tspan>
<!--
alignment-baseline: hanging means the tspan's hanging baseline
aligns to the parent text element's hanging baseline — which is at y=0.
For a 14px font with a cap-height of ~10px and ascender at ~12px:
hanging baseline is typically at the top of the em square.
The text body renders FROM y=0 DOWNWARD — fitting within a 30px viewBox.
BUT if the parent text element has alignment-baseline: ideographic,
the ideographic baseline (which is BELOW the alphabetic baseline) is
aligned to y=0, pulling the entire glyph body above y=0 and out of the viewBox.
-->
</text>
</svg>
// Detection: check for tspan alignment-baseline values + dy offsets in SVG
function detectAlignmentBaselineAttack(svgEl) {
const tspans = svgEl.querySelectorAll('tspan');
for (const tspan of tspans) {
const ab = tspan.getAttribute('alignment-baseline') ||
window.getComputedStyle(tspan).alignmentBaseline;
const dy = parseFloat(tspan.getAttribute('dy') || '0');
const fontSize = parseFloat(window.getComputedStyle(tspan).fontSize || '14');
// Negative dy larger than half the font-size combined with extreme alignment
if (dy < -(fontSize * 0.5) &&
(ab === 'text-top' || ab === 'hanging' || ab === 'mathematical')) {
return { attack: 'alignment-baseline-negative-dy', tspan, ab, dy, fontSize };
}
// Check the tspan's actual rendered position against its parent SVG viewBox
const svgRect = svgEl.getBoundingClientRect();
const tspanRect = tspan.getBoundingClientRect();
if (tspanRect.top < svgRect.top || tspanRect.bottom > svgRect.bottom) {
return { attack: 'alignment-baseline-outside-viewbox', tspan, tspanRect, svgRect };
}
}
return null;
}
Why baseline attacks are particularly dangerous: They exploit the CSS rendering model at a layer below what most security tooling inspects. Dimensional checks pass, visibility checks pass, IntersectionObserver fires — but the text is painted outside its bounding box. The attack requires understanding font metrics, baseline propagation, and the relationship between CSS computed values and SVG coordinate systems. MCP audits without explicit baseline-attack checks will not catch these. See the baseline-source attack reference for per-property detection patterns.
Unified detection approach
The four attacks differ in mechanism but share one key detection primitive: the rendered text position diverges from the element's bounding box. This means a unified detection pass can check for this divergence using Range objects, which give you the bounding rect of the actual text content — not the containing element box.
// Unified baseline attack detector using text-range bounding rects
function detectBaselineAlignmentAttack(consentEl) {
// 1. Get the element's bounding box
const elementRect = consentEl.getBoundingClientRect();
// 2. Get the actual text content bounding rect using a Range
const range = document.createRange();
range.selectNodeContents(consentEl);
const textRects = range.getClientRects(); // one rect per line box of text
if (textRects.length === 0) {
// No text rendered — element may be empty or content may be hidden
return { attack: 'no-text-rects', elementRect };
}
// 3. Compute the union bounding rect of all text line boxes
let textTop = Infinity, textBottom = -Infinity;
for (const rect of textRects) {
textTop = Math.min(textTop, rect.top);
textBottom = Math.max(textBottom, rect.bottom);
}
// 4. Compare text position to element box
const elementTop = elementRect.top;
const elementBottom = elementRect.bottom;
// If text renders above the element box top or below the element box bottom,
// a baseline alignment attack has shifted the text outside the element's box
const shiftAbove = elementTop - textTop; // positive = text above element top
const shiftBelow = textBottom - elementBottom; // positive = text below element bottom
if (shiftAbove > 2 || shiftBelow > 2) {
return {
attack: 'baseline-text-outside-element',
shiftAbovePx: shiftAbove,
shiftBelowPx: shiftBelow,
elementRect,
textTop,
textBottom
};
}
// 5. Also check if the text rect itself is outside the viewport
if (textTop < 0 || textBottom > window.innerHeight) {
return {
attack: 'baseline-text-outside-viewport',
textTop,
textBottom,
viewportHeight: window.innerHeight
};
}
return null;
}
// Usage: call on each consent disclosure element during audit
const consentElements = document.querySelectorAll('[data-consent], .disclosure, #permissions-dialog');
for (const el of consentElements) {
const finding = detectBaselineAlignmentAttack(el);
if (finding) {
console.error('SECURITY: baseline alignment attack detected', finding);
// Log to audit trail, block action, escalate
}
}
The Range.getClientRects() approach returns the actual painted position of text in the document, accounting for all CSS transforms, alignment, and baseline shifts. It is the most reliable method for detecting baseline-based hiding attacks because it bypasses the computed-style layer entirely — if the text is painted outside the viewport, the rects will reflect that regardless of what CSS properties caused it.
Remediation patterns for MCP server authors
MCP server authors who want to ship A-grade audit results need to ensure their consent disclosure elements are immune to baseline-based hiding — including hiding that might be injected by a malicious dependency in their stack. The following patterns are defensive by default.
| Pattern | Why it helps | Implementation |
|---|---|---|
| Block-level disclosure container | Block formatting context is not subject to inline baseline alignment. vertical-align and alignment-baseline do not apply to block-level elements. |
display: block or display: flex (column) on the disclosure container |
| Isolated stacking context | isolation: isolate on the disclosure ensures it forms a new stacking context, unaffected by sibling flex baseline contributions. |
isolation: isolate; align-self: start on the disclosure element inside a flex row |
| Explicit SVG coordinate bounds | For SVG-based disclosures, set overflow="visible" or ensure the y coordinate plus font ascender height is within the viewBox height. |
Calculate: y >= fontSize * 1.2 (ascender clearance) and y + fontSize <= viewBoxHeight |
Freeze vertical-align on all ancestors |
Apply vertical-align: baseline explicitly on all wrapper spans to prevent inherited or injected super/sub accumulation. |
CSS reset on the disclosure subtree: .disclosure, .disclosure * { vertical-align: baseline !important; } |
Runtime Range verification before action |
Before recording user consent, use the Range-based detection above to verify the disclosure text is within the viewport. |
Add a pre-submit check: if (detectBaselineAlignmentAttack(disclosureEl)) { blockAction(); } |
The audit checklist item to add today
SkillAudit's static analysis flags baseline-source: last, vertical-align: super|sub|<length> values above 0.5em, SVG dominant-baseline values other than alphabetic or auto, and alignment-baseline: hanging|ideographic|mathematical on elements in the consent disclosure subtree. Dynamic analysis adds a Range.getClientRects() pass at audit time to catch cases where the baseline shift comes from a containing element rather than the disclosure element itself — the cross-element scenario that static analysis cannot see. If you want your server to carry a green baseline-attack badge before the next Anthropic Skills Directory review cycle, the patterns above are what the audit engine checks. Start with the Pro plan to get full remediation hints per finding.
Baseline alignment attacks sit in an unusual category: they require deep knowledge of CSS font metrics and layout algorithms to construct, but the detection is straightforward once you know to check Range.getClientRects() rather than element bounding boxes. The gap between attack sophistication and detection simplicity makes this a field where automated auditing — not manual review — is the right solution at scale.
For the per-property attack taxonomy with SkillAudit finding codes, see the baseline-source security reference and the full blog archive.