MCP server CSS font-size and line-height consent security
Four SA-CSS-FSZ consent attack patterns that reduce consent text to sub-pixel or zero-height renderings using font-size, line-height, and spacing properties. Unlike color/opacity attacks, these patterns reduce the physical pixel area occupied by consent text to zero or near-zero, making it physically impossible to read regardless of contrast. The text remains in the DOM, is accessible to screen readers, and preserves non-zero layout dimensions in some variants, making detection non-trivial.
WCAG 1.4.3 specifies a minimum contrast ratio (4.5:1 for normal text) but it says nothing about minimum font size. WCAG 1.4.4 requires text to be resizable up to 200% without loss of content, but this requirement is about browser zoom behavior, not about the base rendered font size. An MCP server consent dialog that renders consent text at font-size: 0.1px produces WCAG-compliant contrast (dark text on white background still exceeds 21:1) while making the text physically invisible to any human user. The WCAG auditing criteria are necessary but not sufficient for consent text legibility.
SA-CSS-FSZ-001: font-size:0.1px — sub-pixel height rendering
Setting font-size: 0.1px renders each character at 1/10 of a CSS pixel in height. At 96 DPI (standard display), 1 CSS pixel is approximately 0.265mm. A 0.1px character is 0.0265mm tall — approximately 1/40th of the width of a human hair. At typical viewing distances (50-60cm), this subtends an angle far below the resolution limit of the human visual system. The text is completely invisible to any user without a magnifying glass. The element layout box may still show a computed height reflecting the line box, but that height is negligible.
.consent-text {
font-size: 0.1px;
/* text characters: 0.1px tall = 1/10 of one CSS pixel */
/* at 96 DPI: 0.0265mm = completely invisible to naked eye */
}
/* Layout measurements:
element.getBoundingClientRect().height: ≈ 0 or small value (browser rounding)
element.offsetHeight: depends on browser — may round to 0 or 1px
getComputedStyle(el).fontSize: returns "0.1px" — DETECTABLE
WCAG contrast: unchanged (dark text on white bg = 21:1 — passes!)
opacity: 1 — passes
visibility: visible — passes
Why WCAG passes but consent is impossible:
WCAG 1.4.3 measures contrast of a text pixel relative to its background pixel.
At 0.1px font-size, text pixels ARE present (1 device pixel may be partially occupied).
The contrast of those pixels is high (dark on white).
But the human visual system cannot resolve the text because:
- Letter height 0.1px << minimum resolvable letter height (~4px at screen distance)
- Subpixel rendering may not even output any full-dark pixels at 0.1px font-size
Detection: getComputedStyle(el).fontSize — parse value; flag if computed size < 6px.
CSS minimum legibility threshold: 6px is approximately the lower bound for
any text to be readable even under ideal conditions. */
WCAG does not protect against sub-pixel font sizes: WCAG 1.4.3 contrast requirements apply to text of any size. A 0.1px font renders with correct contrast but is physically invisible. Consent text auditors must check font-size separately from contrast, flagging any computed font-size below a legibility threshold (recommended: 6px minimum for audit purposes).
font-size: 0.1px: text at 0.1/96 inches ≈ invisible; WCAG contrast passes (dark-on-white still 21:1); opacity:1; visibility:visible. Detection: parseFloat(getComputedStyle(el).fontSize) < 6 → CRITICAL.SA-CSS-FSZ-002: font-size:1px — microscopic but technically present
font-size: 1px renders characters at 1 CSS pixel tall — roughly 0.26mm at standard DPI. A user at 50cm viewing distance would need approximately 20/20 vision equivalent to a 5× magnification to resolve individual characters at this size. The element has a non-trivial computed height (1 line × 1px line-box), and the text is technically visible under extreme magnification. Browsers will render some pixels at this font size, and those pixels will have correct contrast. But no ordinary user will read 1px consent text before clicking an Accept button that is sized normally.
.consent-text {
font-size: 1px;
/* 1 CSS pixel = ~0.26mm at 96 DPI */
/* minimum legible text for average adult: ~3mm (≈ 11px on screen) */
/* 1px = approximately 3.6x below minimum legible */
}
/* Threshold analysis for different font sizes:
0.1px: sub-pixel, invisible → SA-CSS-FSZ-001 CRITICAL
1px: microscopic, requires 10x magnification → SA-CSS-FSZ-002 CRITICAL
4px: very small, may be legible with effort for short words → HIGH
6px: minimum for any meaningful readability → HIGH
8px: small but readable with effort → MEDIUM (audit threshold)
10px: small but acceptable for legal fine print → MEDIUM
12px: normal small text → OK for non-primary consent
Recommended audit thresholds:
< 6px: CRITICAL — physically impossible to read without magnification
6-8px: HIGH — extremely difficult to read for most users
8-10px: MEDIUM — difficult for users with low vision
>= 10px: OK (still check WCAG contrast separately)
Detection: parseFloat(getComputedStyle(el).fontSize) — check against thresholds
Note: getComputedStyle returns computed pixel value regardless of unit in stylesheet */
font-size: 1px: characters 0.26mm tall; requires ~10x magnification to read; WCAG contrast passes; element has 1px layout height. Detection: parseFloat(getComputedStyle(el).fontSize) < 6 → CRITICAL.SA-CSS-FSZ-003: line-height:0 — zero line-box height, glyph overlap
CSS line-height controls the height of each line box. Setting line-height: 0 collapses the line box to zero height. Characters are still rendered, but their ink boxes (the actual glyph shapes) extend above and below the baseline, all overlapping at the same vertical position. For multi-line consent text, all lines collapse onto a single horizontal baseline, producing an unintelligible glyph cluster. The element may report zero or near-zero computed height from getBoundingClientRect.
.consent-text {
line-height: 0;
/* line box height: 0px */
/* characters render but all baseline-aligned with zero line separation */
/* for multi-line text: all lines pile up at the same vertical position */
}
/* What the user sees:
Multi-line consent: "By clicking Allow / you grant file system access."
With line-height:0: both lines overlap at the same baseline position.
The glyph ink from both lines overlaps — an illegible dense character cluster.
Single-line consent in a fixed-height container:
line-height:0 collapses the text to zero line box height.
If container has overflow:visible, characters still paint (may be visible as a
single-pixel-height sliver at the baseline).
If container has overflow:hidden, zero line-box height clips the content.
Computed height:
For block element with only text content:
element.getBoundingClientRect().height ≈ 0 (line box is the height)
For inline element:
similar: zero line-box height → zero bounding box height
Detection:
getComputedStyle(el).lineHeight: returns "0px" or "normal" (if overridden by cascade)
parseFloat(getComputedStyle(el).lineHeight) === 0 → SA-CSS-FSZ-003 CRITICAL
If lineHeight is "normal": compute default (≈ 1.2 × fontSize) — not an attack. */
Single-line vs multi-line behavior: For single-line consent text, line-height: 0 collapses the line box but characters may still be partially visible if the container does not clip overflow. The most severe case is multi-line consent text, where all lines overlap into an illegible glyph cluster regardless of overflow. Audit engines should check both the line-height value and whether the text content spans multiple lines (detected by comparing element.clientHeight against computed line height).
line-height: 0: zero line-box height; multi-line text collapses to glyph overlap cluster; element height near zero; text cannot be read. Detection: parseFloat(getComputedStyle(el).lineHeight) === 0 → CRITICAL.SA-CSS-FSZ-004: letter-spacing:-1em — character horizontal overlap collapse
letter-spacing: -1em shifts each subsequent character 1em to the left relative to the previous character. Since 1em equals the current font size, each character overlaps the previous by exactly one full character width (approximately, depending on advance width). For a 14px font, each character shifts 14px left. After 14px/14px = 1 character, the advance width is exactly cancelled by the negative letter-spacing: characters stack on top of each other horizontally. For consent text with normal font sizes, letter-spacing: -1em produces a single-character-width cluster of all overlapping glyphs.
.consent-text {
font-size: 14px;
letter-spacing: -1em; /* = -14px per character */
}
/* Overlap analysis:
Default letter-spacing: 0px (characters spaced by their advance width)
letter-spacing: -1em = -14px per character
Character at position i: x_i = sum(advance_widths[0..i]) + i * (-14px)
For most Latin characters: advance_width ≈ 8-12px
letter-spacing -14px per character: net advance ≈ 8-12 + (-14) = -6 to -2px
Each character moves LEFT relative to the previous character.
After N characters: all characters are within a very narrow horizontal band.
For long text (50+ characters): all characters piled in a ≈ 1-2px wide column.
The user sees: a dense vertical column of overlapping glyph shapes.
No individual character is legible in this cluster.
Detection:
getComputedStyle(el).letterSpacing: returns computed value in px
Threshold: letterSpacing < -0.3em (i.e., < -(0.3 * computedFontSize) px)
At -0.3em: characters are heavily overlapping but some may still be readable.
At -1em: complete collapse.
Flag at < -0.3em for HIGH; < -0.5em for CRITICAL. */
letter-spacing: -1em: characters shifted -1 font-size per glyph; all characters collapse into single-column glyph overlap cluster; text illegible. Detection: parseFloat(getComputedStyle(el).letterSpacing) < -(0.5 * parsedFontSize) → CRITICAL.Summary of SA-CSS-FSZ findings
| ID | Severity | Property | Effective size | Detection |
|---|---|---|---|---|
| SA-CSS-FSZ-001 | CRITICAL | font-size: 0.1px | 0.1px height — sub-pixel invisible | computedFontSize < 6px |
| SA-CSS-FSZ-002 | CRITICAL | font-size: 1px | 1px height — requires 10x magnification | computedFontSize < 6px |
| SA-CSS-FSZ-003 | CRITICAL | line-height: 0 | Zero line box; multi-line glyph overlap | computedLineHeight === 0 |
| SA-CSS-FSZ-004 | CRITICAL | letter-spacing: -1em | All chars collapsed to single-column cluster | letterSpacing < -(0.5 × fontSize) px |