Security reference · CSS injection · Letter spacing · Character overlap · Consent hiding

MCP server CSS letter-spacing consent security

CSS letter-spacing adds uniform extra space after each rendered character. It accepts negative values (characters overlap) and large positive values (characters spread far apart). MCP servers exploit both extremes to make consent text illegible or to expand text past a fixed-width container so most content is clipped. Extreme negative spacing piles all characters into an illegible stack; extreme positive spacing expands text to thousands of pixels with overflow: hidden clipping all but the first word. A dynamic variant makes consent illegible precisely at the moment the user clicks the install button, reverting immediately after. Inherited spacing via an ancestor evades element-level computed-style checks on the consent element itself.

letter-spacing attack surface overview

Attack typeletter-spacing valueEffect on 16px textDetection difficulty
Negative overlap collapse-0.5em (= -8px)Each character overlaps previous by 8px — illegible pileMedium — getComputedStyle returns -8px
Positive overflow expansion5em (= 80px)40-char string = 3200px wide; overflow:hidden clips to ~1 wordMedium — scrollWidth check reveals truncation
Dynamic mousedown attackNormal → -0.5em at mousedownIllegible at click time; static analysis passesHard — requires behavioral/mutation analysis
Inherited from ancestor-0.3em on dialog wrapperAll descendants inherit — consent element own style is cleanHard — must check computedStyle, not element style

letter-spacing is inherited: Unlike overflow or position, letter-spacing is an inherited CSS property. Setting letter-spacing: -0.3em on the dialog container propagates to all descendants including the consent text. An audit that checks the consent element's own style attribute or stylesheet rules (not getComputedStyle) will miss inherited spacing attacks entirely.

Attack 1: extreme negative letter-spacing — character overlap collapse to illegible pile

Negative letter-spacing moves each subsequent character closer to the previous. At -0.5em on 16px text, the spacing is −8px — each character overlaps the previous by 8px. For a typical character width of ~10px, the characters are 80% overlapping. All letters in the consent text pile up near the element's start position, producing an illegible dark smear of overlapping glyphs. The element retains valid color, font-size, display: block, and non-zero dimensions:

/* Malicious CSS — SA-CSS-LS-001 */
.mcp-consent-text {
  /* All other properties normal */
  color: #1a1a1a; /* valid dark text */
  font-size: 14px; /* valid size */
  visibility: visible;
  opacity: 1;

  /* The attack: */
  letter-spacing: -0.5em;
  /* For 14px text: -0.5em = -7px spacing
     Average character advance width: ~9px
     Effective advance per character: 9 − 7 = 2px
     A 40-character consent string renders in ~80px (normally ~360px)
     All characters overlap — the text is a dense smear of glyphs */
}

/* What DOM checks return:
   getComputedStyle(el).color          → "rgb(26, 26, 26)" — valid
   getComputedStyle(el).fontSize       → "14px" — valid
   getComputedStyle(el).letterSpacing  → "-7px" — THIS is the signal
   el.getBoundingClientRect().height   → normal (height not affected)
   el.getBoundingClientRect().width    → much smaller than expected for text length
*/

/* More subtle variant using em-relative values that look reasonable: */
.mcp-consent-v2 {
  letter-spacing: -0.1em; /* -1.4px on 14px — 15% overlap per character */
  /* Less severe than -0.5em but still significantly impairs readability
     especially for lowercase letters and narrow characters (i,l,t,r) */
}

/* Detection: */
function detectNegativeLetterSpacing() {
  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 ls = parseFloat(getComputedStyle(el).letterSpacing);
    if (ls < -1) { /* threshold: any negative letter-spacing */
      findings.push({ id: 'SA-CSS-LS-001', severity: 'high',
        message: `Consent element has letter-spacing: ${ls}px (computed, including inherited). Negative letter-spacing causes character overlap, making consent text illegible. This value may be set directly on the element or inherited from an ancestor.` });
    }
  }
  return findings;
}

Attack 2: extreme positive letter-spacing + overflow:hidden — consent text expanded past container

Large positive letter-spacing expands the text width far beyond any reasonable container. With overflow: hidden, everything past the container boundary is clipped. Only the first 1-2 words of consent text are visible; the rest of the consent is in the DOM but outside the visible area:

/* Malicious CSS — SA-CSS-LS-002 */
.mcp-consent-wrapper {
  overflow: hidden; /* clips expanded text */
  width: 300px; /* shows approximately 3-4 characters at 5em spacing */
}

.mcp-consent-text {
  white-space: nowrap; /* prevents wrapping — text stays on one line */
  letter-spacing: 5em; /* 5em × 16px = 80px between each character */

  /* Effect: "This MCP server will access your files"
     → "T  h  i  s     M  C  P  ..." (80px gap between each character)
     Total width: 40 chars × 80px = 3200px
     Container width: 300px
     Visible: "T  h  i" (3 characters = ~240px)
     Hidden: the rest of the consent text */

  /* This looks like a "wide spacing" design choice — not obviously malicious
     unless the overflow clipping is checked */
}

/* More subtle — the attacker uses a smaller value that is less obvious
   but still expands text past the container: */
.mcp-consent-subtle {
  letter-spacing: 0.8em; /* 12.8px on 16px text — 80% extra spacing */
  overflow: hidden;
  /* A 30-word consent string at 0.8em letter-spacing becomes
     approximately 3× wider than normal — still clips at 300px container */
}

/* Detection: */
function detectPositiveLetterSpacingClip() {
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const ls = parseFloat(getComputedStyle(el).letterSpacing);
    if (ls > 4) { /* >4px letter spacing on consent is suspicious */
      /* Check if clipping is actually occurring */
      if (el.scrollWidth > el.clientWidth) {
        findings.push({ id: 'SA-CSS-LS-002', severity: 'high',
          message: `Consent element has letter-spacing: ${ls}px and is actively overflowing (scrollWidth ${el.scrollWidth}px > clientWidth ${el.clientWidth}px). Extreme positive letter-spacing is expanding the consent text past the visible container.` });
      }
    }
  }
  return findings;
}

Attack 3: dynamic letter-spacing at mousedown — illegible at click time

The consent text is styled normally at page load and during reading. At the mousedown event on the install button, JavaScript sets the consent element's letterSpacing to an extreme negative value. The style reverts at mouseup or click. The consent appears readable during normal browsing; it becomes illegible at the precise moment the user clicks to install:

/* Malicious JS — SA-CSS-LS-003 */
const installBtn = document.querySelector('.mcp-install-button');
const consentEl = document.querySelector('.mcp-consent-text');

installBtn.addEventListener('mousedown', function() {
  /* At click time: collapse consent to illegible pile */
  consentEl.style.letterSpacing = '-0.5em';
  consentEl.style.transition = 'none'; /* instant, no animation */
});

installBtn.addEventListener('mouseup', function() {
  /* Revert immediately after click — no trace in final state */
  consentEl.style.letterSpacing = '';
  consentEl.style.transition = '';
});

/* The attack window is brief — from mousedown to mouseup (~100-300ms).
   During this window, the user is in the act of clicking the install button.
   The consent is illegible at the moment of the consent interaction.

   Why this matters: users instinctively read the consent one more time
   as they move their mouse to the install button (a moment of commitment).
   The attack specifically targets this reading window. */

/* Why static analysis misses it:
   - CSS stylesheets contain no letter-spacing rule on the consent element
   - getComputedStyle() at DOMContentLoaded returns 'normal' (0px)
   - The malicious style is only applied at runtime via event handler
   - MutationObserver on the consent element style attribute would catch it
     IF the observer is running during the interaction */

/* Detection: monitor style mutations during install interaction */
const observer = new MutationObserver(mutations => {
  for (const m of mutations) {
    if (m.attributeName === 'style' && m.target === consentEl) {
      const ls = parseFloat(getComputedStyle(consentEl).letterSpacing);
      if (ls < -1 || ls > 10) {
        findings.push({ id: 'SA-CSS-LS-003', severity: 'high',
          message: `Consent element letter-spacing was mutated during install interaction: ${ls}px. Dynamic letter-spacing applied at click time may make consent illegible during the key consent interaction moment.` });
      }
    }
  }
});
observer.observe(consentEl, { attributes: true, attributeFilter: ['style'] });

Attack 4: inherited letter-spacing from ancestor — evades element-level CSS check

letter-spacing is a CSS inherited property. Setting it on the dialog container applies it to all descendants, including the consent text element. An audit that checks the consent element's own computed style but uses the element.style object (inline styles) or a stylesheet rule targeting the element directly will miss inherited spacing. Only window.getComputedStyle(el).letterSpacing returns the true inherited value:

/* Malicious CSS — SA-CSS-LS-004 */

/* Targeting the container — all descendants inherit */
.mcp-install-dialog {
  letter-spacing: -0.3em;
  /* Applies to ALL text in the dialog — consent, button labels, headings.
     The dialog might look like it uses a "tight tracking" design aesthetic.
     Malicious intent: on small consent text, -0.3em causes 30% character overlap
     making the text significantly harder to read. */
}

/* The consent element itself has no letter-spacing rule: */
.mcp-consent-text {
  /* No letter-spacing here */
  font-size: 14px;
  color: #374151;
}

/* What different checks return:
   el.style.letterSpacing         → "" (no inline style on consent element)
   Stylesheet rule for .mcp-consent-text: no letter-spacing property
   window.getComputedStyle(el).letterSpacing → "-4.2px" (inherited from dialog)

   An auditor that only checks:
   - Element inline style: misses it
   - Direct stylesheet rules for consent element: misses it
   - getComputedStyle on the consent element: CATCHES it
*/

/* Detection: use getComputedStyle, not element.style */
function detectInheritedLetterSpacing() {
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    /* Use getComputedStyle — picks up inherited values */
    const computed = parseFloat(window.getComputedStyle(el).letterSpacing);
    /* Check if the element's own style is "normal" but computed differs */
    const own = el.style.letterSpacing;
    if ((!own || own === 'normal' || own === '') && Math.abs(computed) > 1) {
      findings.push({ id: 'SA-CSS-LS-004', severity: 'medium',
        message: `Consent element has inherited letter-spacing: ${computed}px (element's own style is clean, spacing comes from an ancestor). Inherited negative letter-spacing causes character overlap on consent text.` });
    }
  }
  return findings;
}

letter-spacing attacks preserve valid color, font-size, and DOM content: Standard consent audits check color contrast, font-size minimums, and DOM text content. None of these reveal letter-spacing attacks. An audit checking getComputedStyle(el).color against background color will report valid contrast even when the consent is an illegible pile of overlapping characters. Explicit letterSpacing checks — including inherited values via getComputedStyle — are required to detect this class of attack.

SkillAudit findings for CSS letter-spacing consent attacks

HighSA-CSS-LS-001 — Consent element has negative letter-spacing (computed value, including inherited). Negative spacing causes characters to overlap, making consent text illegible. The extent of overlap depends on the value and font metrics — values below −2px are typically sufficient to cause significant legibility loss on Latin-script consent text.
HighSA-CSS-LS-002 — Consent element has extreme positive letter-spacing with active overflow clipping (scrollWidth > clientWidth). The letter-spacing has expanded the consent text beyond the visible container; content is clipped by overflow: hidden. The user cannot see the full consent text without scrolling a non-scrollable container or removing the CSS.
HighSA-CSS-LS-003 — Consent element style attribute was mutated to apply extreme letter-spacing during an install-button interaction event. The dynamic attack makes consent illegible specifically during the click interaction. Static CSS analysis passes; only mutation observation during the interaction window detects this pattern.
MediumSA-CSS-LS-004 — Consent element has negative or extreme inherited letter-spacing from an ancestor (computed value differs from element's own style). Inherited letter-spacing is frequently missed by audits that check element-level styles instead of getComputedStyle(). Any negative value above −1px threshold should be flagged on consent text elements.

Related MCP consent attack research

SkillAudit's consent audit checks letterSpacing computed values (including inherited) on all consent text elements, compares scrollWidth against clientWidth to detect overflow expansion attacks, and monitors style mutations during install-button interactions to catch dynamic letter-spacing attacks. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-LS findings.