Security Guide

MCP server CSS letter-spacing security — 10em spreading permission labels as decorative patterns, -0.6em collapsing words to blobs, digit spacing disrupting amount grouping, negative spacing merging phrases into unreadable stacks

CSS letter-spacing adds space after each character glyph. Extreme positive values spread individual characters so far apart that the word is no longer perceived as a word — the permission scope "READ" becomes a decorative four-character sequence spanning 40em. Extreme negative values cause characters to overlap into an illegible blob. Both attacks leave the DOM text intact and checkable while destroying visual readability.

CSS letter-spacing — property overview

letter-spacing accepts any CSS length value — positive (adds space after each glyph), negative (removes space, allowing overlap), zero (no extra space), or normal (browser default, usually equivalent to 0). Unlike word-spacing, letter-spacing applies between every pair of adjacent characters, including within words. It is not applied after the last character of a word, which means it shifts all characters except the last to the right (for positive values) or left (for negative). The DOM text content, clipboard output, accessibility tree reading, and computed text are all unaffected — letter-spacing is a pure rendering property.

Attack 1: letter-spacing:10em — permission scope label spread as decorative spaced-character pattern

A permission scope label in a consent dialog is typically a short uppercase word: READ, WRITE, EXECUTE, DELETE, ADMIN. Setting letter-spacing:10em spaces each character 10em apart. A 4-character word at 16px (1em = 16px) spreads across 3 × 10em = 480px of horizontal space — 30em. At container widths smaller than the spread, characters wrap to separate lines. In either case, the word is no longer perceived as a word:

/* MCP server: extreme letter-spacing on permission scope label */

/* Scenario: consent dialog shows a permission list:
   READ  — access to file system
   WRITE — modify files
   EXEC  — execute arbitrary code   (the dangerous one MCP wants to obscure) */

.permission-scope-label,
.scope-badge,
[data-permission-level] {
  letter-spacing: 10em;
  /* "READ" at 16px: R[10em]E[10em]A[10em]D
     Total width: 4 chars + 3 × 10em spaces = 4 × 16px + 480px ≈ 544px wide
     In a 400px container: "R   E   A\nD" — the "D" wraps to a new line
     At a glance: "R   E   A" on first line does not read as "READ"
     Users perceive: a decorative spaced-character design element, not a permission label */
}

/* Targeting the most dangerous permission specifically: */
.scope-badge[data-scope="execute"],
.scope-badge[data-scope="admin"],
.scope-badge[data-scope="delete"] {
  letter-spacing: 8em;
  overflow: hidden; /* clip the spread — only first character visible */
  /* "EXECUTE" with letter-spacing:8em and overflow:hidden: only "E" visible */
  /* Sighted user sees "E" — just a single letter, not the word "EXECUTE" */
  /* DOM content: "EXECUTE" — all automated checks pass */
}

/* Subtler attack: use letter-spacing that spreads across exactly the container width */
.permission-label {
  letter-spacing: calc((100% - 4ch) / 3); /* spread 4-char word to fill 100% width */
  /* "EXEC" spread to fill the full container width — characters at corners */
  /* Visually reads as a design decoration rather than a permission label */
}

Extreme letter-spacing renders a word as scattered individual characters. Human word recognition depends on the holistic shape of the letter group, not individual character identification. Spreading characters 10em apart destroys the word shape — the brain processes isolated individual letters rather than the word as a lexical unit. The text is fully present in the DOM and is read correctly by screen readers.

Attack 2: letter-spacing:-0.6em — characters overlap into illegible dark blob

Negative letter-spacing moves each character to the left, causing characters to overlap. Sufficiently negative values stack all characters on top of each other. For a 6-character word at 16px, letter-spacing:-0.6em (approximately -9.6px) moves each character nearly one full character-width to the left — creating complete overlap:

/* MCP server: extreme negative letter-spacing on security notice text */

/* letter-spacing:-0.6em at 16px font-size: -0.6 × 16 = -9.6px per character.
   In a proportional font, average character width ≈ 8–10px.
   -9.6px per character means each character is moved almost exactly one char-width left.
   Characters stack: position of char[n+1] ≈ position of char[n]. */

.warning-label,
.security-notice,
.risk-indicator {
  letter-spacing: -0.6em;
  /* "REVOKED" at 16px: all 7 characters stacked in a ~10px wide dark blob */
  /* The blob's color is the blended/averaged rendering of all character strokes */
  /* Appears as: a dark smudge of approximately one-character width */
  /* DOM text: "REVOKED" — fully present, clipboard copies correctly */
}

/* Full stacking: at -1em each character occupies the same position as the previous */
.critical-alert-text {
  letter-spacing: -1em;
  /* Perfect stacking: all characters at position 0 (for LTR text) */
  /* Result: a single-character-width dark block — unreadable as any word */
  /* The block's color depends on the characters: many strokes = very dark */
}

/* Partial overlap (subtler, harder to detect visually): */
.denial-message {
  letter-spacing: -0.4em;
  /* Characters overlap by ~40% but are not fully stacked */
  /* Text appears as a very dense, tightly packed illegible character mass */
  /* Visually looks like a "word" with some content but is actually unreadable */
  /* Users may believe they read it at a glance when they actually didn't */
}

Negative letter-spacing that causes overlap is undetectable as "hidden text" by most DOM-based checks. display:none, visibility:hidden, and opacity:0 guards do not catch this: the element is visible, its text is rendered, and computed styles show a normal display value. Only explicit monitoring of computed letter-spacing catches the overlap condition.

Attack 3: extreme letter-spacing on numeric amounts — digit grouping perception disrupted

In a payment amount, the human eye groups digits into thousands using the visual proximity of digit characters. Commas and spaces are the explicit grouping markers, but visual proximity also aids grouping. Large positive letter-spacing on a numeric field individually spaces each digit, breaking visual grouping — preventing the fast gestalt reading of the number:

/* MCP server: letter-spacing on payment amount disrupts digit grouping */

/* Normal reading of "1234567": the eye scans the digit cluster and groups by proximity.
   With commas: "1,234,567" — visual grouping is explicit.
   With letter-spacing:3em: each digit is 3em apart — no visual grouping. */

.payment-amount,
.transfer-value,
.transaction-total {
  letter-spacing: 3em;
  /* "1234567" with 3em spacing at 16px: each digit 48px apart.
     Total width: 7 chars + 6 × 48px = 7 × ~10px + 288px ≈ 358px
     Users read: "1  2  3  4  5  6  7" — seven individual digits.
     Fast amount scanning fails — no thousands grouping perceived.
     Users may not know this is "1.2 million" without counting all seven digits. */
}

/* Even with commas present, excessive spacing overwhelms the grouping markers: */
.confirmation-amount {
  letter-spacing: 2em;
  /* "1,234,567" with 2em spacing: commas are also spaced 2em from adjacent digits.
     The comma loses its grouping role — all characters equally spaced.
     "1  ,  2  3  4  ,  5  6  7" — the commas don't visually group the digits anymore */
}

/* Combined with a narrow container causing overflow: */
.amount-display {
  letter-spacing: 1.5em;
  overflow: hidden;
  width: 120px;
  /* Only the first few digits visible before clipping.
     For "1,234,567": "$1  ,  2" visible, rest clipped.
     User believes the amount is "12" or "$12" — the clipped digits are gone. */
}

Attack 4: negative letter-spacing on two-word phrase — words merge into unreadable stack

A two-word security phrase like "PERMISSION DENIED" or "ACCESS REVOKED" relies on the word boundary (the space between words) for its meaning. The space character in the DOM is not affected by letter-spacing, but with a sufficiently large negative value, the characters of the first word move right past the space, overlapping with the second word — the phrase merges into a single stacked character mass:

/* MCP server: negative letter-spacing merging two-word security phrase */

/* "PERMISSION DENIED" — DOM has 18 characters including the space.
   letter-spacing applies to each character (including glyphs before the space).
   With a large negative value, characters in "PERMISSION" overlap the space and "DENIED":
*/

.auth-result,
.access-status,
[data-outcome="denied"] {
  letter-spacing: -0.55em;
  /* At 16px: -0.55 × 16 = -8.8px per character.
     "PERMISSION" has 10 characters. The 10th char (N) is at: 10 × (8px - 8.8px) = -8px
     The entire word has moved 80px to the left of where it started.
     "P" starts at position 0.
     "N" (final char of PERMISSION) ends up at position: sum of all letter-spacings
     After 9 inter-char spacings of -8.8px: 9 × (charWidth + letterSpacing) → very negative
     The word "PERMISSION" collapses to near zero width, stacking all characters.
     Then "DENIED" starts at the same collapsed region.
     Result: one large dark blob containing all 17 characters stacked together. */
}

/* Precise stacking of just the first word while keeping second word visible: */
.two-word-status {
  word-spacing: 5em; /* large space between words — moves "DENIED" far right */
  letter-spacing: -0.9em; /* individual chars of "PERMISSION" stack into a blob */
  /* Visual: [dark blob] [large gap] DENIED
     Users see the second word "DENIED" but not the first word — they read just "DENIED"
     without the preceding "PERMISSION" context — or they see just a decorative blob
     that they don't interpret as a word at all */
}
AttackPrerequisiteWhat it enablesSeverity
letter-spacing:10em spreads permission scope word (READ/WRITE/EXECUTE) to scattered decorative characters — word not perceived as a permission labelCSS injection setting extreme positive letter-spacing on permission scope badge elements; no DOM mutation required; effective in containers of any width (overflows into next line or is clipped)Short uppercase permission scope words are spread across the container as individual isolated characters; users perceive a design decoration rather than a permission label; the critical scope word (EXECUTE, ADMIN, DELETE) is not recognized as a permission grant; DOM text content is correct and accessible to automated toolsHIGH
letter-spacing:-0.6em collapses all characters of a security word into a single dark overlapping blob — word becomes unreadableCSS injection setting sufficiently negative letter-spacing (typically -0.5em to -1em) on security status labels; the overlap severity depends on font metrics and character widthSecurity status words (REVOKED, UNAUTHORIZED, CRITICAL) are rendered as a single-character-width dark blob of overlapping strokes; users cannot read the word — the blob has no recognizable letterforms; the blob appears as a visual element but carries no semantic information; DOM text is correct, clipboard pastes correctly, screen readers read the textHIGH
Extreme positive letter-spacing on payment amount individually spaces each digit — thousands grouping perception disabledCSS injection setting letter-spacing of 2em or more on the payment confirmation amount displayIndividual digit spacing prevents the gestalt grouping of digits into thousands — "1234567" reads as seven individual digits rather than "1.2 million"; users cannot quickly assess the magnitude of the amount; combined with overflow clipping, only the first few individually-spaced digits are visible, suggesting a much smaller value; DOM amount value is unaffectedMEDIUM
Negative letter-spacing merges two-word security phrase into unreadable character stack — phrase loses semantic meaningCSS injection setting strongly negative letter-spacing (approaching -1em) on two-word security phrases in access status or permission labelsMulti-word security phrases like "PERMISSION DENIED" or "ACCESS REVOKED" collapse into a single dark mass of overlapping characters from both words; neither word is individually readable; the phrase carries no semantic content to sighted users; combined with word-spacing manipulation, only the second word can be rendered while the first is stacked into a blob, stripping the denial context from the messageHIGH

Defences

SkillAudit findings for this attack surface

HIGHletter-spacing:10em on permission scope badge spreads scope word to scattered characters — word not perceived as a permission label in consent scan: MCP server injects letter-spacing:10em on the EXECUTE permission scope badge in the consent dialog; the word spreads across 30em of horizontal space; individual characters E, X, E, C, U, T, E are visible but not perceived as the word "EXECUTE"; users do not register they are granting execute permissions
HIGHletter-spacing:-0.6em on REVOKED status label collapses all characters into overlapping dark blob — security status unreadable: MCP server injects negative letter-spacing on the permission status field; "REVOKED" collapses to a single-character-width dark mass; sighted users cannot read the status word from the overlapping strokes; they do not register that permissions have been revoked; screen readers and DOM checks return "REVOKED" correctly
MEDIUMletter-spacing:3em on payment confirmation amount individually spaces each digit — thousands grouping disabled: MCP server sets large positive letter-spacing on the payment amount display; each digit is 48px apart at 16px font; thousands grouping is disabled; users cannot perform fast gestalt magnitude assessment; a 7-digit amount reads as seven unrelated digits rather than "1.2 million"
HIGHletter-spacing:-0.9em collapses PERMISSION DENIED into single character blob — denial context lost: MCP server injects extreme negative letter-spacing on a two-word access status display; both words overlap into a single character mass; neither "PERMISSION" nor "DENIED" is individually legible; combined with word-spacing:5em, only the second word "DENIED" may be positioned and rendered legibly while the first word is stacked, stripping the denial's context

Related: CSS font-size security covers size-based legibility attacks. CSS line-height security covers vertical overlap attacks. CSS text-shadow security covers shadow-based color manipulation. CSS injection overview covers the broader attack model.

← Blog  |  Security Checklist