Security Guide
MCP server CSS white-space-collapse security — collapse merging disclosure newlines onto one clipped line, preserve enabling whitespace injection, break-spaces hiding content after wide space gaps, discard stripping permission delimiter spaces
CSS white-space-collapse is a new spec-level property (Chrome 114+, Firefox 124+) that separates whitespace collapsing behavior from line-wrapping behavior, splitting the legacy white-space shorthand into two independent axes. For MCP servers injecting CSS, it provides fresh attack vectors: collapsing source newlines in multi-line disclosures merges everything onto one overflow-clipped line; preserving injected whitespace makes DOM-injected space sequences render as visual gaps; break-spaces alters how wrapping handles space sequences, pushing content off-screen; and discard strips meaningful delimiter spaces from permission scope strings.
CSS white-space-collapse — property overview
The legacy white-space property controlled both how whitespace characters in the HTML source are collapsed and whether the element wraps at soft wrap opportunities. The CSS Text Level 4 specification separates these into white-space-collapse (collapsing behavior: collapse, preserve, preserve-breaks, preserve-spaces, break-spaces, discard) and text-wrap (wrapping behavior: wrap, nowrap, balance, stable). The legacy white-space shorthand remains for backwards compatibility and maps to combinations of these new properties. white-space-collapse is supported in Chrome 114+, Firefox 124+, and Safari 17.4+.
Attack 1: white-space-collapse:collapse on multi-line disclosure — source newlines removed, content merged onto one overflow-clipped line
A multi-line security disclosure written with intentional HTML newlines or <br> breaks relies on those line breaks to separate key disclosure points. The collapse value removes all source newlines (treating them as whitespace to be collapsed) — merging the multi-point disclosure into one continuous text run that wraps normally but, in a container with overflow:hidden and a fixed height, clips everything after the first line:
/* white-space-collapse:collapse on security disclosure with fixed-height container */
.permission-disclosure,
.security-notice,
.consent-statement {
white-space-collapse: collapse;
/* 'collapse' is actually the default. But in elements that used
white-space:pre or white-space:pre-wrap (to preserve their intentional breaks),
overriding back to collapse removes the preserved newlines.
If the host intended the disclosure to use pre-formatted newlines:
white-space: pre-wrap; → preserves \n as line breaks, wraps at container edge
MCP injects:
white-space-collapse: collapse; → overrides the pre-wrap collapse setting.
Result: the \n characters in the source are collapsed to spaces.
The multi-point disclosure merges into one long line. */
}
/* Combined with overflow:hidden and single-line height: */
.disclosure-container {
white-space-collapse: collapse; /* removes intentional breaks */
overflow: hidden;
max-height: 1.5em; /* approximately one line height */
}
/* A 10-point disclosure list (originally 10 lines) collapses to one run-on line.
Only the first ~60-80 characters are visible in the 1.5em container.
The remaining 9 disclosure points are present in the DOM but clipped.
DOM text: all 10 points present and complete.
Visual: only the first point visible. */
This attack is most effective against disclosures that rely on white-space:pre or white-space:pre-wrap for their multi-line structure. Such elements use source newlines (not <br> elements) for their line breaks — collapsing the whitespace merges all content. Elements that use <br> or separate <p> tags for line breaks are not affected by white-space-collapse.
Attack 2: white-space-collapse:preserve — injected whitespace in DOM text renders as visual gaps
In contrast to collapsing, white-space-collapse:preserve makes all whitespace characters in the text content significant — spaces, tabs, and newlines are rendered as-is. This enables whitespace injection: if the MCP server has injected runs of space characters into a security text field via DOM manipulation (or if the server response includes many spaces that are normally collapsed), enabling preservation makes those spaces render as wide visual gaps:
/* MCP server injects spaces into security text via DOM manipulation */
/* Step 1: MCP server sets a DOM text node in the security status display */
document.querySelector('.auth-status').textContent =
'AUTHORIZED TO READ FILESYSTEM';
/* 14 spaces between AUTHORIZED and TO READ FILESYSTEM */
/* Normally collapsed: "AUTHORIZED TO READ FILESYSTEM" (single space) */
/* Step 2: MCP server injects CSS to preserve the whitespace */
.auth-status {
white-space-collapse: preserve;
}
/* Result: "AUTHORIZED TO READ FILESYSTEM"
The 14-space gap creates a large visual chasm between "AUTHORIZED" and "TO READ FILESYSTEM".
The user reads the first word "AUTHORIZED" in the normal reading scan area.
The rest of the string starts far to the right — may be off the visible container edge.
DOM text: "AUTHORIZED TO READ FILESYSTEM" — correct.
Visual: "AUTHORIZED" visible, "TO READ FILESYSTEM" pushed off-screen by the space gap. */
/* Tab character injection (even more powerful):
Tab characters with white-space-collapse:preserve render at the element's tab-size.
With tab-size:200px (or tab-size:100 characters), a single tab creates 200px gap.
.auth-status { white-space-collapse: preserve; tab-size: 200px; }
DOM text: "AUTHORIZED\tFILESYSTEM_READ" (one tab between words)
Visual: "AUTHORIZED" [200px gap] "FILESYSTEM_READ" — tail clips off right edge. */
Attack 3: white-space-collapse:break-spaces — trailing spaces in wrapped lines remain, creating wide gaps
The break-spaces value is similar to preserve but with a key difference: spaces at the end of a line that would normally be hidden (they are "hanging spaces" after the last word before wrap) are preserved and rendered. This means runs of spaces can push visible content to the right and off the container edge even when text wrapping is active:
/* white-space-collapse:break-spaces — trailing spaces push content off-screen */
.permission-label {
white-space-collapse: break-spaces;
/* Unlike normal behavior where trailing spaces at line-end are hidden,
break-spaces renders them. */
}
/* MCP server's DOM content for permission label:
"READ FILE WRITE"
(20 spaces between "FILE" and "WRITE")
Normal collapse: "READ FILE WRITE" (spaces collapsed to one)
With break-spaces: spaces after "FILE" are preserved and rendered.
In a 300px container:
- "READ FILE " wraps at the container edge (first wrap opportunity after spaces).
- The 20 rendered spaces on that first line occupy ~100px (5px per space at 16px).
- After wrapping, the remaining spaces start the next line.
- "WRITE" eventually appears after the rendered space run.
- The space run may push "WRITE" out of the visible container area.
Key security impact: a permission scope label "READ FILE WRITE"
can have the "WRITE" scope rendered outside the visible area of the consent
dialog label cell, while "READ FILE" is visible and the injected spaces look
like a design gap. "WRITE" is in the DOM but not visible to sighted users. */
Attack 4: white-space-collapse:discard — all whitespace stripped from permission scope strings
The discard value removes all whitespace characters from the rendered text — spaces, tabs, and newlines are completely discarded and do not render at all. For permission scope strings that use spaces as delimiters between multiple scope keywords, this merges all tokens together without separator:
/* white-space-collapse:discard — whitespace stripped entirely */
.permission-scope-list,
.granted-permissions,
[data-scopes] {
white-space-collapse: discard;
}
/* DOM text: "READ WRITE EXECUTE DELETE FILESYSTEM"
(space-delimited permission scope list)
With discard: all spaces removed.
Visual: "READWRITEEXECUTEDELETEFILESYSTEM"
The spaces that separated the five scope keywords are gone.
Users see an unreadable merged string that doesn't pattern-match as individual permission words.
They may perceive it as a single long technical identifier rather than five separate scopes.
The most dangerous scopes (EXECUTE, DELETE) are buried in the merged string.
Combined with overflow:hidden and narrow container:
Only the first N characters visible: "READWRI"
The user sees what appears to be a partial technical identifier.
They do not see EXECUTE, DELETE, or FILESYSTEM in the clipped merged string. */
/* Even simpler use: strip the space in a two-word phrase */
.auth-result {
white-space-collapse: discard;
}
/* "PERMISSION DENIED" → "PERMISSIONDENIED"
The phrase runs together without the word boundary.
Users may read this as a single 18-character technical term, not as "PERMISSION DENIED".
Word recognition for both words fails when they are concatenated. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| white-space-collapse:collapse overriding pre-wrap on multi-line disclosure — source newlines removed, all content merged onto one line, only first portion visible in fixed-height container | Host consent UI must use white-space:pre or pre-wrap for its multi-line structure (rather than <br> elements); CSS injection of white-space-collapse:collapse overrides the preserve-breaks behavior; container must have overflow:hidden and fixed/max height; supported Chrome 114+, Firefox 124+ | A 10-point disclosure list collapses to one long run-on line; in a container with max-height:1.5em and overflow:hidden, only the first disclosure point (first ~60-80 chars) is visible; remaining 9 points are in the DOM but completely hidden; DOM check returns all content correct; the attack is specific to pre-formatted disclosure structures | HIGH |
| white-space-collapse:preserve enabling whitespace injection — MCP-injected space runs in DOM text render as visual gaps pushing content off-screen | MCP server can both inject whitespace into DOM text nodes (via JS) and inject CSS setting white-space-collapse:preserve on the target element; tab-size injection amplifies the effect (one tab = tab-size px gap); Chrome 114+, Firefox 124+ | Space or tab characters injected into security status text render as wide visual gaps when preservation is enabled; "AUTHORIZED [20 spaces] TO READ FILESYSTEM" shows "AUTHORIZED" in visible area, "TO READ FILESYSTEM" pushed off-screen; users read partial security status without the scope context | HIGH |
| white-space-collapse:break-spaces — trailing spaces in wrapped lines are preserved and rendered, pushing subsequent scope keywords off the visible container edge | CSS injection of white-space-collapse:break-spaces; MCP server can inject space sequences into permission scope list text (via DOM, not just CSS); container has fixed width; Chrome 114+, Firefox 124+ | Permission scope labels with MCP-injected space sequences between tokens (e.g. "READ FILE [20 spaces] WRITE") have the trailing spaces rendered after wrap — "WRITE" scope keyword appears after the space run off the right edge of the visible container; "WRITE" is in the DOM but not visible to sighted users | MEDIUM |
| white-space-collapse:discard stripping all whitespace from permission scope list — scope keywords merged without separator, individual scopes unrecognizable in merged string | CSS injection of white-space-collapse:discard on the permission scope list element; the host must use space-delimited scope lists (not separate DOM elements for each scope); Chrome 114+, Firefox 124+ | Space-delimited scope list "READ WRITE EXECUTE DELETE FILESYSTEM" collapses to "READWRITEEXECUTEDELETEFILESYSTEM" — users see an unreadable technical-looking string, not five individual permission scope words; dangerous scopes EXECUTE and DELETE are buried mid-string; combined with overflow clip, only the first few characters are visible | HIGH |
Defences
- Use
<br>elements instead of source newlines for disclosure line breaks.white-space-collapsecannot collapse<br>elements — they are block-level line breaks that are unaffected by whitespace collapsing. Multi-line disclosures built with<br>or separate<p>tags are immune to the collapse attack. - Use separate DOM elements for each permission scope keyword. Instead of a space-delimited text string like "READ WRITE EXECUTE", use separate
<span class="scope">READ</span> <span class="scope">WRITE</span>elements. Thediscardattack only affects whitespace within text nodes — inter-element space is structural and not collapsed bywhite-space-collapse. - Freeze
white-space-collapse(andwhite-space) on critical elements with!important. Addwhite-space-collapse: collapse !important;(to prevent preserve/break-spaces injection) to text elements where normal collapse is desired, andwhite-space-collapse: preserve-breaks !important;to elements where newline preservation is structurally important. - Computed style check: audit
white-space-collapseon security-critical elements. Any value other than the expected default (collapsefor normal elements,preserveorpreserve-breaksfor pre-formatted ones) on security text elements should be flagged.discardon any element is always suspicious. - SkillAudit flags:
white-space-collapse:discardon any element;white-space-collapse:preserveorbreak-spaceson permission scope or security status elements;white-space-collapse:collapseon elements that use source newlines as structural separators for disclosure points.
SkillAudit findings for this attack surface
Related: CSS tab-size security covers tab character width manipulation pushing content off-screen. CSS text-overflow security covers overflow clipping of security text. CSS overflow:clip security covers the distinction between overflow:hidden and overflow:clip. CSS injection overview covers the broader attack model.