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 */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| letter-spacing:10em spreads permission scope word (READ/WRITE/EXECUTE) to scattered decorative characters — word not perceived as a permission label | CSS 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 tools | HIGH |
| letter-spacing:-0.6em collapses all characters of a security word into a single dark overlapping blob — word becomes unreadable | CSS injection setting sufficiently negative letter-spacing (typically -0.5em to -1em) on security status labels; the overlap severity depends on font metrics and character width | Security 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 text | HIGH |
| Extreme positive letter-spacing on payment amount individually spaces each digit — thousands grouping perception disabled | CSS injection setting letter-spacing of 2em or more on the payment confirmation amount display | Individual 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 unaffected | MEDIUM |
| Negative letter-spacing merges two-word security phrase into unreadable character stack — phrase loses semantic meaning | CSS injection setting strongly negative letter-spacing (approaching -1em) on two-word security phrases in access status or permission labels | Multi-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 message | HIGH |
Defences
- CSP
style-srcwith nonce. Prevents injection of<style>blocks settingletter-spacing. The complete solution. - Monitor computed
letter-spacingon security-critical elements. Any value outside a small safe range (e.g.-0.05emto0.2em) on permission scope labels, payment amounts, or audit grade displays should be flagged. Legitimate UI designs rarely need letter-spacing beyond ±0.1em on security text. - Freeze
letter-spacingon critical elements with!important. In your host CSS, addletter-spacing: normal !importanton all permission status labels, amount displays, and warning text. Injected rules at equal or lower specificity cannot override!important. - Use canvas or SVG for rendering critical amounts. Canvas-rendered text is not subject to CSS
letter-spacing. For final payment confirmation screens, render the amount via canvas to prevent any CSS-based visual manipulation. - SkillAudit flags:
letter-spacingvalues greater than1emon any text element;letter-spacingvalues more negative than-0.15emon any text element; anyletter-spacingon elements whose text content is a known permission scope keyword or numeric amount.
SkillAudit findings for this attack surface
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.