Security reference · CSS injection · Line height · Line box collapse · Consent hiding
MCP server CSS line-height consent security
CSS line-height defines the height of each line box within a block container. It is one of the most fundamental layout properties — and one of the most overlooked attack vectors for consent hiding. A value of 0 collapses all line boxes to zero height, piling every line of text at the same baseline position into an illegible stack of overlapping characters. An extreme positive value like 50em spreads each line 800px apart for 16px text — a 4-line consent block spans 3200px, making lines 2 through 4 permanently below the viewport fold. A fractional value clips glyph ascenders and descenders. A dynamic attack applies zero line-height precisely at install-button mousedown. All four patterns leave color, font-size, and DOM content intact.
line-height attack surface overview
| Attack type | line-height value | Effect on 16px/4-line consent | getBoundingClientRect().height |
|---|---|---|---|
| Zero collapse | 0 or 0px | All lines at same baseline — illegible pile | 0px (or min-height/padding if set) |
| Extreme positive spread | 50em = 800px | 4 lines span 3200px — lines 2-4 below fold | ~3200px (element is tall) |
| Fractional below cap-height | 0.5 = 8px | Glyphs extend above/below 8px line box — clipped by overflow:hidden | Depends on container |
| Dynamic mousedown collapse | normal → 0 at mousedown | Illegible at click time — static analysis passes | Normal at load; 0 during click |
line-height affects visual layout without changing font-size or color: getComputedStyle(el).fontSize and getComputedStyle(el).color are unaffected by line-height. A zero line-height consent element reports valid font-size and color contrast — only explicitly checking getComputedStyle(el).lineHeight and comparing it to the font's cap-height metric reveals the attack.
Attack 1: line-height: 0 — all text lines collapse to overlapping baseline pile
Setting line-height: 0 reduces every line box to zero height. The text glyphs overflow the zero-height line boxes equally above and below the baseline. Multiple text lines all render at the same baseline position — each line's characters overprint the previous line's characters. The result is an illegible superimposition of all consent text at a single vertical position. The element's height collapses to zero (or to any explicit min-height or padding):
/* Malicious CSS — SA-CSS-LH-001 */
.mcp-consent-paragraph {
/* All other properties valid */
color: #1a1a1a;
font-size: 14px;
display: block;
visibility: visible;
/* The attack: */
line-height: 0;
/* or equivalently: line-height: 0px */
/* All consent lines render at the same Y position.
Line 1: "This MCP server will access..."
Line 2: "your files and send content to..."
Line 3: "external servers on your behalf."
All three lines overprint each other — dense smear of overlapping glyphs. */
/* Optionally: padding prevents the element from having zero height,
so it appears to have content (getBoundingClientRect shows non-zero height) */
padding: 8px 0;
min-height: 24px; /* element appears to have 3 lines of height */
}
/* What DOM checks return:
getComputedStyle(el).fontSize → "14px" — valid
getComputedStyle(el).color → "rgb(26, 26, 26)" — valid
getComputedStyle(el).lineHeight → "0px" — THIS is the signal
el.textContent → full consent text (in DOM)
el.getBoundingClientRect().height → 24px (from min-height) — not zero
*/
/* Detection: */
function detectZeroLineHeight() {
const findings = [];
const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
for (const el of document.querySelectorAll('*')) {
if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
const lh = getComputedStyle(el).lineHeight;
const lhNum = parseFloat(lh);
const fs = parseFloat(getComputedStyle(el).fontSize);
if (lhNum === 0) {
findings.push({ id: 'SA-CSS-LH-001', severity: 'high',
message: `Consent element has line-height: 0. All text lines render at the same baseline position — the consent text is an illegible overlap of all lines. DOM text is present; visual rendering is destroyed.` });
} else if (lhNum > 0 && lhNum < fs * 0.6) {
/* Less than 60% of font-size — severe glyph overlap */
findings.push({ id: 'SA-CSS-LH-001', severity: 'high',
message: `Consent element has line-height: ${lhNum}px which is less than 60% of font-size: ${fs}px. Lines will overlap significantly, making consent text hard to read.` });
}
}
return findings;
}
Attack 2: extreme positive line-height — consent text spread across 3200px
Large line-height values create enormous gaps between text lines. For a 4-line consent block at line-height: 50em on 16px text, the line boxes are each 800px tall — the first line is visible at the top of the install dialog; lines 2-4 are at y=800px, y=1600px, and y=2400px respectively, all far below any visible viewport without significant scrolling. Combined with no visible scroll indicator (overflow: hidden on the consent container), the remaining lines are permanently inaccessible:
/* Malicious CSS — SA-CSS-LH-002 */
.mcp-consent-disclosure {
font-size: 14px;
line-height: 50em; /* 50 × 14px = 700px per line */
/* A 4-line consent block:
Line 1: "This server will access your files" → y: 0 to 700px
Line 2: "and send content to external" → y: 700 to 1400px
Line 3: "servers including your SSH keys" → y: 1400 to 2100px
Line 4: "and email credentials." → y: 2100 to 2800px
*/
}
/* Container: clips everything below 200px */
.mcp-consent-container {
overflow: hidden; /* or max-height with no scrollbar */
max-height: 200px;
/* Only line 1 fits — 700px line-box, but glyph is at top of line box
so the first line's characters are visible in the first ~20px.
Lines 2-4 are entirely outside the 200px container. */
}
/* More subtle variant: line-height large enough to put line 2 below the viewport
WITHOUT requiring overflow:hidden — the element is just very tall */
.mcp-consent-no-clip {
line-height: 20em; /* 320px per line on 16px text */
/* Line 2 starts at 320px below the fold of a typical 300px install dialog.
The user can scroll to it, but there is no visual cue that more content exists
below the first line — the dialog appears to show the full consent. */
}
Attack 3: fractional line-height below cap-height — glyph ascender/descender clipping
A line-height smaller than the font's cap-height creates line boxes too short to contain the full glyph height. With overflow: hidden on the consent container sized to line-height × number-of-lines, the ascenders and descenders of glyphs are clipped. Uppercase letters and descender characters (g, j, p, q, y) lose their distinguishing features — "g" and "q" become ambiguous, uppercase and lowercase become visually similar, and the consent text is harder to read accurately:
/* Malicious CSS — SA-CSS-LH-003 */
.mcp-consent-container {
overflow: hidden;
/* Height exactly matches line-height × lines — no room for glyph overflow */
height: calc(0.5em * 3); /* 3 lines at 0.5em line-height = 1.5em total */
}
.mcp-consent-text {
font-size: 16px;
line-height: 0.5; /* 0.5 × 16px = 8px line-box */
/* For a typical font:
Cap height: ~11px (68% of 16px font-size)
Ascender height: ~12px
Descender depth: ~4px below baseline
Line box height: 8px
Each glyph overflows the 8px line-box by:
- 3px above (ascenders clipped)
- 4px below (descenders clipped)
The overflow is clipped by the parent overflow:hidden container.
Capital letters lose their tops; descender letters (g,p,q,y) lose tails.
The text is technically present but significantly degraded. */
}
/* Specific impact on consent keywords:
"GRANT" → "G A N T" with tops of letters clipped (less recognizable)
"access" → "acce s" with descender of the second "s" variant clipped
"password" → "passwor" with descender of "d" shape altered */
Attack 4: JS line-height mutation at mousedown — zero line-height at click time
Same dynamic pattern as the letter-spacing attack variant: consent text has valid line-height: 1.5 at page load and during reading. At the mousedown event on the install button, JavaScript sets lineHeight = '0'. The style reverts at mouseup. Static CSS analysis finds nothing wrong; only behavioral/mutation analysis during the interaction window detects the attack:
/* Malicious JS — SA-CSS-LH-004 */
const installBtn = document.querySelector('.mcp-install-button');
const consentEl = document.querySelector('.mcp-consent-text');
installBtn.addEventListener('mousedown', function(e) {
/* At click time: collapse all lines to zero height */
consentEl.style.lineHeight = '0';
/* Instant — no transition — collapses in one frame */
});
installBtn.addEventListener('mouseup', function() {
/* Restore immediately — no evidence in post-click state */
consentEl.style.lineHeight = '';
});
/* The attack interval:
mousedown → mouseup: typically 50-200ms for a normal click
During this window, the consent is an illegible pile of lines.
On a fast double-click, the window is even shorter.
More aggressive variant: touch events on mobile */
installBtn.addEventListener('touchstart', function() {
consentEl.style.lineHeight = '0';
}, { passive: true });
installBtn.addEventListener('touchend', function() {
consentEl.style.lineHeight = '';
}, { passive: true });
/* Detection: MutationObserver on the consent element */
const observer = new MutationObserver(mutations => {
for (const m of mutations) {
if (m.attributeName === 'style') {
const lh = parseFloat(getComputedStyle(consentEl).lineHeight);
if (lh === 0 || lh < parseFloat(getComputedStyle(consentEl).fontSize) * 0.5) {
findings.push({ id: 'SA-CSS-LH-004', severity: 'high',
message: `Consent element line-height was mutated during interaction: ${lh}px. Dynamic line-height: 0 applied at install-button mousedown makes consent illegible during the click interaction window.` });
}
}
}
});
observer.observe(consentEl, { attributes: true, attributeFilter: ['style'] });
line-height attacks are not detectable by color or font-size checks: Standard consent audits verify that text color meets WCAG contrast ratios and that font-size meets minimum thresholds. line-height: 0 destroys visual legibility while reporting valid color contrast and valid font-size from getComputedStyle(). Auditors must explicitly check getComputedStyle(el).lineHeight and compare it to getComputedStyle(el).fontSize to identify collapse (lh < fs × 0.6) and extreme spread (lh > fs × 10) patterns.
SkillAudit findings for CSS line-height consent attacks
line-height: 0 (or a value below 60% of font-size). All text lines render at the same baseline position — the consent text is a visually illegible overlap of all lines. DOM text and color contrast remain valid; only the line-height value reveals the attack.line-height (above 10× font-size) creating line boxes so large that text lines 2 and beyond fall below the visible viewport or container boundary. The first line is visible; the remaining consent text requires scrolling past a gap measured in hundreds of pixels.line-height below the font cap-height ratio (below 0.7× font-size) combined with overflow: hidden on a fixed-height ancestor. Glyph ascenders and descenders are clipped, degrading the legibility of uppercase letters and descender characters in the consent text.line-height was mutated to 0 or a near-zero value during an install-button interaction event. The dynamic attack makes consent illegible during the click window then reverts immediately. Static analysis passes; mutation observation during install-button interactions is required to detect this pattern.Related MCP consent attack research
- CSS letter-spacing attacks — character overlap collapse and overflow expansion of consent
- CSS font-size clamp() attacks — clamped sub-threshold font sizes
- CSS font-variant attacks — small-caps glyph size collapse
- CSS resize attacks — collapse consent container to zero requiring drag interaction
- CSS filter effects as a consent bypass vector
SkillAudit's consent audit checks lineHeight computed values on all consent text elements, flags zero/near-zero values (collapse attack) and extreme positive values (spread attack), and monitors style mutations during install-button interactions to catch dynamic line-height attacks. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-LH findings.