← Blog

MCP server CSS letter-spacing collapse consent security

CSS letter-spacing adds inter-character spacing after each glyph advance. Negative values reverse this: a letter-spacing:-1em on 16px text creates a −16px advance after every character, pulling all subsequent characters back to the element origin. The result is all characters rendered at the same position — an illegible pile of overlapping glyphs that covers a single pixel point. textContent reads the full string. getBoundingClientRect may return collapsed or near-zero width. Four SA-CSS-LSP patterns document the collapse attack surface, including word-spacing and compound sub-pixel approaches.

By SkillAudit  |  30 Oct 2026

CSS text spacing properties — letter-spacing, word-spacing — are additive adjustments to the advance widths computed by the font shaping engine. Unlike font-size, they do not directly modify glyph metrics; they modify the cursor advance after each glyph is placed. A sufficiently negative value eliminates and reverses the advance, causing glyphs to pile up rather than progress across the baseline.

The critical property of collapse attacks is that textContent, innerText, and all DOM text APIs remain fully functional. Screen readers that derive consent from the accessibility tree still announce the full text. Spell-checkers and text-search tools find the content. Only the visual rendering is destroyed, making this attack maximally effective against sighted users while potentially passing automated audits that check text presence rather than text geometry.

SA-CSS-LSP-001: letter-spacing:-1em — full character advance reversal

A letter-spacing of -1em on text with font-size:16px sets the inter-character spacing to -16px. The net advance width for each character is: glyph advance − 16px. For standard Latin characters, glyph advances at 16px range from approximately 4px (narrow characters like ‘i’) to 14px (wide characters like ‘W’). In all cases, the net advance is negative. The cursor moves backward after each character. After the first character, all subsequent characters are placed to the left of the previous one, accumulating into a dense pile at the element start position. With overflow:hidden on the parent, only the pile at the start position is visible, and even that pile of overlapping characters is illegible.

/* Attack: full em-unit negative letter-spacing */
.consent-text {
  font-size: 16px;
  letter-spacing: -1em; /* = -16px: reverses cursor after each glyph */
  /* Net glyph advance: glyph_width - 16px < 0 for all standard chars.
     All characters pile at origin. BoundingClientRect width ≈ max(glyph_width).
     textContent: full string readable. Visual rendering: illegible pile. */
}

/* Variants: */
.consent-text {
  font-size: 16px;
  letter-spacing: -16px; /* Same effect: pixel-unit equivalent */
}

.consent-text {
  font-size: 16px;
  letter-spacing: -0.5em; /* = -8px: partial collapse; chars overlap 50% */
  /* 'B' (advance ~11px) + (-8px spacing) = 3px net advance → dense but detectable pileup */
}

/* Detection:
   const ls = parseFloat(getComputedStyle(el).letterSpacing);
   const fs = parseFloat(getComputedStyle(el).fontSize);
   // letterSpacing returns computed px value (e.g. -16px)
   if (ls < 0 && Math.abs(ls) >= fs * 0.5) → CRITICAL
   // If |letter-spacing| >= 50% of font-size: chars overlap by >50%
   // If |letter-spacing| >= font-size: full collapse at origin

   Also check: getBoundingClientRect(el).width
   // Collapsed text → width ≈ single glyph width or 0
   // Compare to expected text width: el.scrollWidth vs clientWidth */

DOM text is readable while visual rendering is destroyed: el.textContent returns the full consent string for a letter-spacing:-1em element. Text-search and screen-reader APIs find the content. Only a numeric check on the computed letterSpacing value relative to fontSize catches this attack.

CRITICAL
SA-CSS-LSP-001 — letter-spacing: -1em: reverses character advance; all glyphs pile at element origin; textContent intact. Detection: parseFloat(letterSpacing) < 0 && Math.abs(letterSpacing) ≥ fontSize * 0.5 → CRITICAL.

SA-CSS-LSP-002: letter-spacing:-0.5em + font-size:8px — compound small-font collapse

Combining a moderate negative letter-spacing with a small font-size achieves collapse through a different ratio. At font-size:8px, glyph advances are approximately 2–7px. A letter-spacing:-0.5em = -4px makes the net advance of many characters zero or negative. The attack uses plausible-looking values: -0.5em is a moderate tracking reduction that passes casual review, while 8px is a small but not obviously-zero font size. Together they produce collapse that neither value alone signals as clearly malicious.

/* Attack: compound — moderate letter-spacing + small font collapses together */
.consent-text {
  font-size: 8px;          /* small but non-zero */
  letter-spacing: -0.5em; /* = -4px at 8px font: moderate-looking */
  /* Net advance: glyph_advance(8px) - 4px
     Narrow chars ('i','l','1'): advance ≈ 2px → net = -2px (collapse)
     Medium chars ('a','e','o'): advance ≈ 5px → net = +1px (near-zero)
     Wide chars ('M','W','m'): advance ≈ 7px → net = +3px (slight progress)
     Result: non-uniform partial collapse; consent unreadable as a string */
}

/* Detection: compound check */
const ls = parseFloat(getComputedStyle(el).letterSpacing);
const fs = parseFloat(getComputedStyle(el).fontSize);
// Normalize: letter-spacing as fraction of font-size
const lsRatio = Math.abs(ls) / fs;
if (ls < 0 && lsRatio >= 0.3 && fs < 12) → CRITICAL
// High |ls/fs| ratio at small font-size: compound collapse
if (ls < 0 && lsRatio >= 0.5) → CRITICAL
// Regardless of font-size: >50% of em = collapse for most chars

Compound attacks use individually-plausible values: -0.5em letter-spacing is a common design pattern for tight tracking in headlines. 8px is used for legal fine-print. Together they produce collapse in consent text. Detection must evaluate the ratio of |letter-spacing| to font-size, not each property in isolation.

CRITICAL
SA-CSS-LSP-002 — letter-spacing: -0.5em + font-size: 8px: compound collapse; individually plausible; combined produces partial-to-full character overlap. Detection: evaluate |letterSpacing| / fontSize ratio ≥ 0.5 → CRITICAL.

SA-CSS-LSP-003: word-spacing:-100px — inter-word gap collapse

CSS word-spacing adjusts the additional space added between words (beyond the space character advance). A value of -100px subtracts 100px from every inter-word gap. For normal body text where word advances are typically 40–80px, this makes the net inter-word spacing strongly negative: words overlap into each other. Consent text like “By clicking Allow, you agree to our Terms of Service and Privacy Policy” collapses into a 1-2 word-width blob where all words pile at the element start. Unlike letter-spacing, word-spacing only affects spaces between words — intra-word character spacing is preserved. This produces a different illegibility pattern: characters within words are readable individually but words run together beyond recognition.

/* Attack: extreme negative word-spacing collapses all words together */
.consent-text {
  word-spacing: -100px;
  /* Space character advance (~4-6px) + (-100px) = -94 to -96px net.
     Each word moves ~94px to the left after the previous word.
     All words overlap at the start of the element.
     BoundingClientRect width ≈ width of longest single word. */
}

/* Moderate variant — less obvious: */
.consent-text {
  word-spacing: -50px;
  /* Still collapses words for normal 12-16px text.
     Consent text "By clicking Allow you agree..."
     after 5 words at -50px: all 5 words overlap within ~50px width. */
}

/* Detection:
   const ws = parseFloat(getComputedStyle(el).wordSpacing);
   // Returns computed px value; 'normal' → 0
   if (ws < -20) → HIGH   (words overlap on standard text)
   if (ws < -50) → CRITICAL (full word collapse for typical consent length)
   // Threshold depends on typical word width; -50px catches all fonts */

word-spacing affects only inter-word gaps: Characters within each word remain at correct spacing. An auditor checking letterSpacing misses this attack entirely. Check wordSpacing independently. A value more negative than -20px is suspicious for any consent text; more negative than -50px is always an attack.

CRITICAL
SA-CSS-LSP-003 — word-spacing: -100px: all words collapse into overlapping pile at element start; intra-word spacing preserved but inter-word gaps negative. Detection: parseFloat(getComputedStyle(el).wordSpacing) < -50 → CRITICAL.

SA-CSS-LSP-004: letter-spacing:-1em with text-shadow ghost — shadow-only visibility

An advanced variant combines letter-spacing:-1em with a text-shadow that uses a positive offset to create a ghost copy of the text at a readable position. The shadow is rendered by the browser using the same glyph outlines, but positioned by the shadow offset rather than the character advance. The shadow is visible at the offset coordinates while the actual text characters pile at the origin. This pattern creates a visual appearance of visible text — the shadow appears at a reasonable position — while the DOM text characters are still collapsed. The shadow is not accessible to screen readers (it is a visual effect only) and the consent characters remain at the collapsed position for DOM interaction purposes.

/* Attack: collapsed text + shadow creates ghost text appearance */
.consent-text {
  letter-spacing: -1em;  /* collapse all characters to origin */
  color: transparent;    /* make actual characters invisible */
  text-shadow: 16px 0 0 rgba(0,0,0,0.3); /* ghost: offset right by 1em */
  /* Result: shadow appears at +16px offsets from each collapsed position.
     Since all characters are at origin, all shadows pile at +16px from origin.
     The shadow is also a collapsed pile — just at position x+16 instead of x.
     Casual visual inspection: "there's text there, it's just faint"
     Reality: all shadow characters overlap at a single point. */
}

/* More sophisticated variant using positive letter-spacing for shadow: */
.consent-text::before {
  content: attr(data-consent); /* data attribute holds consent text */
  letter-spacing: normal;       /* shadow copy has normal spacing */
  color: rgba(0,0,0,0.1);      /* very faint — hard to read but present */
}
.consent-text {
  letter-spacing: -1em;  /* actual text collapsed */
  color: transparent;
}
/* Shadow-copy pseudo-element appears readable but at 10% opacity.
   Actual consent text collapsed and transparent.
   Auditors checking textContent find text. Auditors checking opacity miss shadow.
   Only compound check catches this: collapsed actual + near-transparent shadow. */

/* Detection:
   Check: letter-spacing extreme AND (color:transparent OR opacity near-zero)
   This combination with any visible shadow or pseudo-element is CRITICAL.
   Check ::before/::after pseudo-elements independently for faint shadow copies. */

Shadow-only text creates a false appearance of readability: A faint shadow copy of collapsed text looks like “it’s just styled small” to a visual reviewer. The actual consent text is transparent and collapsed. Detection requires checking both the actual element properties and any pseudo-elements for compound collapse+transparency patterns.

CRITICAL
SA-CSS-LSP-004 — letter-spacing:-1em + color:transparent + text-shadow: shadow provides false visual; actual text collapsed and invisible. Detection: collapsed letterSpacing + transparent color + any text-shadow → CRITICAL.

Summary of SA-CSS-LSP findings

IDSeverityProperty combinationCollapse mechanismDetection
SA-CSS-LSP-001 CRITICAL letter-spacing: -1em Full em-unit reversal; all chars at origin |letterSpacing| ≥ fontSize → CRITICAL
SA-CSS-LSP-002 CRITICAL letter-spacing: -0.5em + font-size: 8px Compound: moderate spacing × small font = collapse |letterSpacing| / fontSize ≥ 0.5 → CRITICAL
SA-CSS-LSP-003 CRITICAL word-spacing: -100px Inter-word gap reversal; words pile at start wordSpacing < −50px → CRITICAL
SA-CSS-LSP-004 CRITICAL letter-spacing:-1em + color:transparent + text-shadow Shadow ghost creates false visual; text collapsed+transparent Collapsed letterSpacing + transparent + text-shadow → CRITICAL

Related pages