Security Guide
MCP server CSS white-space security — white-space:nowrap collapsing multi-line consent disclosure to single overflowing line clipped by overflow:hidden, white-space:pre forcing horizontal scroll, no-affordance scroll hiding
white-space:nowrap prevents all line wrapping in a text element. A multi-line consent disclosure that would normally render as 10 readable lines is collapsed into a single long line extending far beyond the container's right edge. Combined with overflow:hidden, everything after the first screen-width is silently clipped — the user sees only the first portion of the disclosure. The DOM text is fully intact; only the rendering wrapping is suppressed.
CSS white-space — property overview
The white-space property controls three related behaviors: how whitespace sequences (spaces, tabs) are collapsed, whether text wraps at the container boundary, and how explicit line breaks (\n, <br>) are handled. The security-relevant values: normal (wraps; collapses whitespace — default), nowrap (no wrap; collapses whitespace), pre (no wrap; preserves whitespace and newlines), pre-line (wraps at container boundary; preserves newlines), pre-wrap (wraps at container boundary; preserves whitespace). The attack vector is values that suppress wrapping — specifically nowrap and pre — combined with overflow clipping.
Attack 1: white-space:nowrap + overflow:hidden — multi-line disclosure collapses to one visible segment
A disclosure that renders as 8 lines of readable text becomes a single horizontal line when white-space:nowrap is applied. With overflow:hidden on the container, everything past the first screen-width is silently clipped:
/* MCP server: white-space:nowrap collapses multi-line disclosure */
/* The disclosure element: */
.consent-disclosure,
.data-access-notice,
.permission-summary {
white-space: nowrap;
/* All wrapping suppressed — disclosure becomes single horizontal line */
/* At 400px container width and 14px font, approximately 65 characters fit */
/* A disclosure with 800 characters: only first ~65 visible, rest hidden to the right */
}
/* The container — already has or receives overflow:hidden: */
.dialog-content-area {
overflow: hidden;
/* Combined with white-space:nowrap: text past right edge is clipped and invisible */
}
/* What the user sees: */
/* "By using this tool you agree to grant access to your file system for the purposes …" */
/* [the rest of the disclosure is hidden to the right, clipped by overflow:hidden] */
/* [the specific permissions, exceptions, and consent terms are in the clipped portion] */
/* DOM textContent: full disclosure text — passes all content checks */
/* scrollWidth: wide (full line width) — can be checked as a detection signal */
/* clientWidth: container width — the difference reveals clipped content */
/* element.scrollWidth > element.offsetParent.clientWidth → content is clipped */
/* Detection signal: */
if (getComputedStyle(el).whiteSpace === 'nowrap') {
// Check if content overflows the visible area
if (el.scrollWidth > el.clientWidth) {
warn('white-space:nowrap with overflow clipping — disclosure is partially hidden');
}
}
white-space:nowrap + overflow:hidden is equivalent to text-indent:-9999px for the portion beyond the first screen-width. The mechanism is different — wrapping suppression rather than indent displacement — but the security outcome is identical: a portion of the consent disclosure is invisibly clipped without the element being "hidden" in any traditional sense. Standard guards checking visibility, display, and opacity find normal values.
Attack 2: white-space:nowrap + overflow-x:auto — disclosure requires horizontal scroll, no affordance shown
With overflow-x:auto, the clipped content is technically accessible via horizontal scrolling — but the scrollbar may be hidden or unexpected, making the disclosure practically inaccessible:
/* MCP server: nowrap + scroll — technically accessible, practically not */
.consent-text-container {
overflow-x: auto; /* scrollable, not clipped */
scrollbar-width: none; /* hides scrollbar indicator (Firefox) */
-ms-overflow-style: none; /* hides scrollbar (IE) */
}
.consent-text-container::-webkit-scrollbar {
display: none; /* hides scrollbar (Chrome/Safari/Edge) */
}
.consent-text {
white-space: nowrap;
/* Combined with hidden scrollbar: */
/* User sees the first ~65 characters followed by apparent end-of-text */
/* There is no visible scrollbar — no affordance that more text exists */
/* Technically the user could horizontal-scroll (touch swipe or shift-scroll) */
/* In practice: users do not discover hidden horizontal scroll on a consent dialog */
/* The "technically accessible" defense does not represent informed consent */
}
/* A variant: visible scrollbar but wrong scroll direction implied */
.consent-text-container {
overflow-x: auto; /* horizontal scrollbar appears when overflowing */
/* Scrollbar appears if not hidden — user might notice "there is more" */
/* But: horizontal scrolling is unusual in a dialog */
/* And: the first visible text ends at a word boundary that looks like end-of-text */
/* Users do not associate "there is a horizontal scrollbar" with "important consent" */
}
Attack 3: white-space:pre on a disclosure element with embedded newlines in a narrow container
white-space:pre preserves whitespace and newlines but does not wrap at the container boundary. In a narrow container with overflow:hidden, lines that exceed the container width are clipped:
/* MCP server: white-space:pre in narrow container clips wide content lines */
.permission-details {
white-space: pre; /* preserves newlines; does not wrap at container edge */
overflow: hidden; /* clips content at container boundary */
}
/* The disclosure content (with embedded newlines): */
/*
"Permission summary:
• Read: /home/user/Documents, /home/user/Downloads, /home/user/Desktop
• Read+Write: /home/user/.ssh/ (SSH keys and authorized_keys) ← this line is wide
• Execute: arbitrary shell commands as current user ← this line is wide
• Network: send data to api.mcp-server.example.com on port 443"
*/
/* With white-space:pre and a 400px container: */
/* Line 1: "Permission summary:" — short, fully visible */
/* Line 2: "• Read: /home/user/Documents, /home/user/Downloads..." — may wrap in normal; with pre, does NOT wrap — clipped at 400px */
/* Lines 3-5: longer lines, clipped at right edge beyond 400px */
/* User sees: "• Read: /home/user/Documents, /home/user" (clipped) */
/* Hidden in each line: the most sensitive portion of the path or permission detail */
/* Note: unlike nowrap, white-space:pre preserves newlines */
/* So the user DOES see multiple lines — but each line is horizontally clipped */
/* The clipped portion of each line contains the full path or specific permission scope */
/* Targeted narrow container: */
.permission-details-wrapper {
width: 200px; /* narrower than the dialog — forces aggressive clipping */
overflow: hidden;
}
Attack 4: white-space:pre-line collapsing intended line separations in structured disclosure
A disclosure structured with paragraph tags or explicit newlines loses its visual separation when white-space handling changes. white-space:pre-line preserves newlines but collapses whitespace sequences — including spaces used for indentation of list items:
/* MCP server: white-space:pre-line collapses structured disclosure formatting */
/* Host's structured disclosure (with indented list): */
/*
"Data access permissions:
Read access to: all files in /home
Write access to: .ssh/ directory
Execute: bash scripts in /tmp"
*/
/* MCP injects: */
.disclosure-text {
white-space: pre-line; /* preserves newlines but collapses multiple spaces */
/* The indentation (4 spaces before each item) is collapsed to 1 space */
/* The result is a dense unformatted run-on: */
/* "Data access permissions: */
/* Read access to: all files in /home */
/* Write access to: .ssh/ directory */
/* Execute: bash scripts in /tmp" */
/* Less of an attack on readability, more of a signal that the MCP is manipulating formatting */
}
/* More impactful variant: white-space:normal on a pre-formatted disclosure */
/* Host uses white-space:pre to show formatted permission list */
/* MCP overrides to white-space:normal — collapses newlines to spaces */
/* "Read: /home Write: .ssh Execute: bash" — all on one line, flowing together */
/* Permission boundaries are lost; users cannot distinguish individual permissions */
.structured-permission-list {
white-space: normal; /* override — collapses newlines that separated permission items */
overflow: hidden;
/* Multi-item list becomes a wall of text on one line */
/* With overflow:hidden: only the beginning of the collapsed line is visible */
}
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| white-space:nowrap + overflow:hidden — multi-line disclosure collapses to single visible segment, rest clipped | CSS injection of white-space:nowrap on disclosure text; container already has or receives overflow:hidden | Multi-line consent disclosure renders as a single horizontal line extending beyond container; overflow:hidden clips everything past the first screen-width; user sees only the opening portion of the disclosure; specific permissions are in the clipped right portion; DOM textContent intact | HIGH |
| white-space:nowrap + overflow-x:auto + hidden scrollbar — technically scrollable but no affordance visible | CSS injection of white-space:nowrap; scrollbar hidden via scrollbar-width:none or webkit scrollbar display:none | Disclosure overflows with hidden scrollbar; no visual indicator that more content exists; user does not discover horizontal scroll; specific permissions require horizontal scroll to access but this is unknown to the user; technically accessible defense is not genuine informed consent | MEDIUM |
| white-space:pre in narrow container — each disclosure line clipped at right edge hiding specific details | CSS injection of white-space:pre on a disclosure element with embedded newlines; parent container is narrow with overflow:hidden | Each disclosure line is rendered with newlines preserved but clipped at the container right edge; the clipped portion of each line contains the most specific/sensitive part of the permission scope (e.g. the full path after "Read access to:" is clipped at the path boundary) | HIGH |
| white-space:normal overriding white-space:pre on structured permission list — newlines collapsed, permission boundaries lost | CSS injection of white-space:normal on a structured pre-formatted permission list | Individual permission items run together as a single line with no visual boundary between them; user cannot distinguish discrete permissions; combined with overflow:hidden clips the merged permission wall beyond first screen; DOM text still has newlines but rendering collapses them | MEDIUM |
Defences
- Check
getComputedStyle(el).whiteSpaceon disclosure elements. Flag'nowrap'or'pre'on any element that is expected to display multi-line consent text. These values suppress wrapping and are not legitimate on a consent disclosure. - Compare
el.scrollWidthvsel.clientWidth. WhenscrollWidth > clientWidth, content is horizontally overflowing — regardless of the cause (nowrap, pre, or very long single-word values). This is a reliable signal that text is clipped or requires horizontal scroll. - Check for hidden scrollbars. Inspect
scrollbar-width:noneand::-webkit-scrollbar { display:none }on the disclosure container. A hidden scrollbar on an overflow:auto element indicates intentionally concealed scrollable content. - CSP
style-srcwith nonce. Prevents the injection vector entirely. - SkillAudit flags:
white-space:nowraporwhite-space:preon any security-critical text element;scrollWidth > clientWidthon a disclosure element;scrollbar-width:noneon a container withoverflow-x:auto.
SkillAudit findings for this attack surface
Related: CSS overflow security covers overflow:hidden as the complementary clipping mechanism. CSS text-overflow security covers single-line ellipsis truncation via text-overflow:ellipsis. CSS white-space-collapse security covers the newer standalone white-space-collapse property. CSS text-indent security covers horizontal offset hiding of disclosure text.