← Blog

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.

By SkillAudit  |  30 Oct 2026

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).

CRITICAL
SA-CSS-FSZ-001 — 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 */
CRITICAL
SA-CSS-FSZ-002 — 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).

CRITICAL
SA-CSS-FSZ-003 — 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. */
CRITICAL
SA-CSS-FSZ-004 — 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

IDSeverityPropertyEffective sizeDetection
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

Related pages