MCP server CSS line-height and height collapse consent security
CSS line-height:0 collapses all line boxes to zero height: each line of consent text is placed at the same baseline position, stacking all lines into a zero-height pile of overlapping glyphs. Container collapse via height:0; overflow:hidden clips the entire content regardless of font metrics. Both techniques leave textContent, scrollHeight, and DOM text APIs fully accessible — only visual rendering is destroyed. Four SA-CSS-LHT patterns document the height-collapse attack surface for consent text.
Line-height and container-height collapse attacks exploit the separation between CSS box model geometry and rendered content. CSS text layout first places glyph bitmaps, then sizes line boxes around them according to line-height, then positions the element according to height. Setting line-height or height to zero does not remove content from the DOM; it collapses the box geometry that would make content visible. Content remains fully accessible to APIs that read the DOM, not the pixel output.
These attacks are structurally distinct from opacity and color attacks (which destroy pixel values) and from transform attacks (which move content outside the viewport). Height collapse keeps content at normal document coordinates but makes its vertical extent zero — it is “there” in the layout model, just occupying zero vertical pixels.
SA-CSS-LHT-001: line-height:0 — zero line box stack
CSS line-height sets the height of line boxes in a block formatting context. A value of 0 sets all line boxes to zero height. The baseline for each line is still computed (font metrics still apply), but the line box collapses to a zero-height box around the baseline. In a multi-line consent paragraph, all lines are placed at the same vertical position — each line of text overlaps the previous. The result is all consent lines stacked at a single coordinate, forming an illegible pile of glyph bitmaps. The total element height depends on how many lines exist: with line-height:0 and no explicit height, the element collapses to near-zero height (browser-specific: some browsers apply a minimum).
/* Attack: zero line-height collapses all lines to single baseline */
.consent-text {
line-height: 0;
/* All line boxes: height=0.
Line 1: baseline at y=0.
Line 2: baseline at y=0 (same position).
Line 3: baseline at y=0 (same position).
All glyphs from all lines overlap at baseline.
Result: illegible pile; element height ≈ 0. */
}
/* With overflow:hidden — clips even the baseline pile: */
.consent-container {
line-height: 0;
overflow: hidden;
/* overflow:hidden clips at element edges.
With line-height:0, element height → 0.
overflow:hidden clips at height=0.
All content clipped away entirely. */
}
/* Detection:
const lh = getComputedStyle(el).lineHeight;
// Returns computed px value or 'normal'
if (lh !== 'normal') {
const lhPx = parseFloat(lh);
if (lhPx === 0) → CRITICAL (SA-CSS-LHT-001: zero line-height stack)
if (lhPx < 4) → CRITICAL (near-zero: multi-line consent collapses)
}
Also: scrollHeight > clientHeight detects clipped content
if (el.scrollHeight > el.clientHeight) → content exists outside clip rect */
line-height:0 with overflow:hidden is a double-collapse: line-height:0 alone collapses all lines to a single baseline, producing an illegible pile. Adding overflow:hidden clips even that pile. Both conditions together make consent text entirely invisible while leaving it fully present in the DOM and accessible tree.
line-height: 0: all line boxes at zero height; all lines at same baseline; element collapses; textContent intact. Detection: parseFloat(getComputedStyle(el).lineHeight) < 4 → CRITICAL.SA-CSS-LHT-002: height:0 + overflow:hidden — container clip collapse
Setting an explicit height:0 on a consent container element with overflow:hidden clips all child content to zero pixels. The container box has zero height; the overflow:hidden rule clips all content that extends beyond the container boundary. Any child element, including consent text with normal line-height and font-size, is clipped completely. scrollHeight on the container will return a positive value (the natural content height), revealing that hidden content exists. clientHeight returns 0. This contrast — scrollHeight > 0, clientHeight === 0 — is a reliable detection signal for container-clip consent attacks.
/* Attack: container height:0 clips all child consent content */
.consent-container {
height: 0;
overflow: hidden;
/* Container has zero declared height.
overflow:hidden clips at height=0.
All children — regardless of their own height — are clipped away.
Consent text element has normal font-size and line-height.
But it is inside the zero-height clipped container.
Result: nothing visible. */
}
.consent-text {
/* This element may have completely normal styles: */
font-size: 16px;
line-height: 1.5;
color: #111;
/* ...but none of it renders — it's inside the clipped container. */
}
/* Detection on the consent CONTAINER:
const container = consentTextEl.closest('.consent-container');
const ch = container.clientHeight; // → 0
const sh = container.scrollHeight; // → e.g. 120px
if (ch === 0 && sh > 0) → CRITICAL (container clip collapse)
Equivalently: check for explicit height:0 with overflow:hidden
on any ancestor of consent text:
let el = consentTextEl.parentElement;
while (el) {
const cs = getComputedStyle(el);
if (cs.overflow === 'hidden' && parseFloat(cs.height) === 0) → CRITICAL;
el = el.parentElement;
} */
The attack is on the ancestor, not the consent element: getComputedStyle(consentTextEl) may show normal font-size, color, and display. The attack is on an ancestor container. Auditors must traverse the ancestor chain and check for zero-height containers with overflow:hidden. Compare scrollHeight vs clientHeight at every container ancestor.
height:0; overflow:hidden: container clips all consent content; scrollHeight > 0 reveals hidden text. Detection: traverse ancestors for height:0 + overflow:hidden; check scrollHeight > clientHeight → CRITICAL.SA-CSS-LHT-003: max-height:0 + overflow:hidden — accordion pattern weaponized
The max-height:0; overflow:hidden pattern is a standard CSS accordion/collapse component technique: an element transitions from max-height:0 (collapsed) to max-height:500px (expanded) on interaction. When this pattern is applied to a consent container statically — without the expansion interaction — it collapses consent text to zero height permanently. The pattern looks legitimate in CSS review because it is a well-known UI pattern. An auditor unfamiliar with the pattern may not flag it as an attack. Unlike height:0, max-height:0 does not require a computed height of exactly zero: the max-height constraint is the operative ceiling.
/* Attack: accordion collapse pattern applied statically to consent */
.consent-container {
max-height: 0;
overflow: hidden;
/* Standard accordion CSS — BUT: no trigger to expand it.
Consent text is permanently collapsed behind max-height:0.
Looks like: "there's an expand button somewhere" (there isn't).
Or: looks like an intentional compact UI choice. */
}
/* Variation: nearly-zero max-height shows 1px sliver */
.consent-container {
max-height: 2px;
overflow: hidden;
/* 2px visible: just enough to show a hairline of glyph pixels.
From a distance: "there's text there, just very small."
Reality: only 2px of a 16px font-size element is visible. */
}
/* Detection:
Check max-height on all consent container ancestors:
const mh = parseFloat(getComputedStyle(containerEl).maxHeight);
// Returns px value; 'none' → Infinity
if (mh === 0 && getComputedStyle(containerEl).overflow === 'hidden')
→ CRITICAL (max-height:0 accordion collapse)
if (mh < 8 && getComputedStyle(containerEl).overflow === 'hidden')
→ CRITICAL (near-zero max-height clips text to hairline)
Key insight: max-height CSS reports the VALUE of max-height, not whether
it is actually constraining content. Combine with scrollHeight check:
if (containerEl.scrollHeight > containerEl.clientHeight) → clipped */
Accordion CSS patterns are a legitimate design technique being weaponized: max-height:0; overflow:hidden is in many CSS frameworks for collapsible sections. When found on a consent container with no JavaScript trigger to expand it, it is a consent attack. Always verify that a collapsed accordion has a working expand mechanism — and that the consent text is in the expanded state, not the collapsed one.
max-height:0; overflow:hidden on consent ancestor: standard accordion pattern applied permanently; consent clipped. Detection: max-height === 0 + overflow:hidden + scrollHeight > clientHeight → CRITICAL.SA-CSS-LHT-004: line-height:0.01 — near-zero unitless collapse
A unitless line-height is a multiplier on font-size. line-height:0.01 on a font-size:16px element produces a computed line-height of 0.16px. Each line box is 0.16px tall. For a 3-line consent paragraph, the total line-height space is 0.48px — far below any visible pixel. The glyphs — which extend above and below the baseline regardless of line-height — may still partially render depending on the browser’s handling of glyph overflow from line boxes. But the layout height of the element is 0.48px: effectively zero. An overflow:hidden ancestor clips at this near-zero height. The value 0.01 is a unitless number, and its attack nature may not be obvious to auditors that only check for line-height:0 exactly.
/* Attack: near-zero unitless line-height multiplier */
.consent-text {
font-size: 16px;
line-height: 0.01; /* = 0.01 × 16px = 0.16px per line */
/* 3-line consent: total line-height space = 0.48px
Glyph cap-height at 16px ≈ 11px → extends 10.5px above 0.16px line box
Some browsers clip glyph overflow; others don't.
overflow:hidden on parent clips all content at 0.48px.
Result: entirely invisible in any clipped context. */
}
/* Variant: unitless below normal threshold: */
.consent-text { line-height: 0.1; } /* = 1.6px at 16px font — still near-zero */
.consent-text { line-height: 0.5; } /* = 8px at 16px font — hairline line boxes */
/* Detection threshold:
const lhRaw = getComputedStyle(el).lineHeight;
if (lhRaw !== 'normal') {
const lhPx = parseFloat(lhRaw);
const fsPx = parseFloat(getComputedStyle(el).fontSize);
const lhRatio = lhPx / fsPx;
// 'normal' is typically 1.2
if (lhRatio < 0.5) → CRITICAL (line boxes less than 50% of font-size)
// A 3-line consent at lhRatio=0.5 has total height = 1.5× font-size
// vs normal 3.6× — severely compressed
} */
Check the line-height-to-font-size ratio: A unitless line-height:0.01 is not obviously an attack from the value alone. The key check is the ratio of computed lineHeight (in px) to computed fontSize (in px). A ratio below 0.5 produces line boxes smaller than the font’s own metrics, causing overlap and near-zero element height. Flag ratios below 0.5 as CRITICAL.
line-height: 0.01 (unitless near-zero): 0.16px line boxes at 16px font; total consent height sub-pixel; clips to nothing with overflow:hidden. Detection: lineHeight/fontSize ratio < 0.5 → CRITICAL.Summary of SA-CSS-LHT findings
| ID | Severity | CSS property | Collapse mechanism | Detection |
|---|---|---|---|---|
| SA-CSS-LHT-001 | CRITICAL | line-height: 0 | All lines at same zero-height baseline; element collapses | parseFloat(lineHeight) < 4 → CRITICAL |
| SA-CSS-LHT-002 | CRITICAL | height:0 + overflow:hidden (ancestor) | Container clips all child content at zero height | ancestor height:0 + overflow:hidden; scrollHeight > clientHeight |
| SA-CSS-LHT-003 | CRITICAL | max-height:0 + overflow:hidden (ancestor) | Accordion pattern weaponized statically; consent permanently collapsed | ancestor max-height:0 + overflow:hidden + no expand trigger |
| SA-CSS-LHT-004 | CRITICAL | line-height: 0.01 (unitless) | Sub-pixel line boxes; total element height sub-pixel; clips with overflow | lineHeight(px) / fontSize(px) ratio < 0.5 → CRITICAL |