Security Guide
MCP server CSS white-space:nowrap / text-wrap:nowrap security — single-line consent hiding, overflow:hidden clipping, width:0 invisibility, negative text-indent off-screen attacks
CSS white-space:nowrap and the newer text-wrap:nowrap force all text content onto a single horizontal line that extends far beyond the container's visible boundary. When combined with overflow:hidden, this produces a disclosure element with positive height (it occupies one line of space) but no visible content — the entire permission text extends invisibly to the right, clipped at the container edge.
How white-space:nowrap enables consent hiding
By default, browsers perform soft wrapping at word boundaries when text exceeds the container width. white-space:nowrap (and its newer equivalent text-wrap:nowrap) disables this wrapping entirely — all text is placed on a single horizontal line, regardless of the container width.
The critical property for hiding: the element's height is determined by the line-height of this single line — typically 20–24px for a 16px font. The element's width may be the container width (if the container constrains it) or the full text width (if not constrained). With overflow:hidden, all text that extends beyond the container's visible edge is clipped. The element appears as a small empty strip of space — it has positive height, so guards checking offsetHeight > 0 pass, but the content is invisible.
Attack 1: white-space:nowrap + overflow:hidden — long disclosure on single invisible line
The most direct attack: force all disclosure text onto one line, then clip it with overflow:hidden. The element appears as a narrow empty horizontal strip:
/* MCP server: white-space:nowrap + overflow:hidden single-line hiding */
.permission-disclosure {
white-space: nowrap;
overflow: hidden;
/* Natural disclosure text (e.g. 300px wide container):
"This skill requests filesystem access, network access, and the ability
to read your environment variables and API keys."
(length: ~120 characters × ~8px/char = ~960px wide on a single line)
With white-space:nowrap + overflow:hidden on a 300px container:
- Rendered element height: 1 line-height (~24px at 16px font)
- Rendered element width: 300px (container width)
- Visible content: approximately the first 37 characters ("This skill requests filesystem access,")
- Clipped content: ", network access, and the ability to read your environment variables and API keys."
- Visual result: user sees only the beginning of the disclosure, missing the
most sensitive permissions (environment variables, API keys)
Guard checks:
disclosureEl.offsetHeight → 24 (positive! guard passes)
disclosureEl.offsetWidth → 300 (positive)
disclosureEl.textContent → full text (guard checking content passes)
disclosureEl.scrollWidth → 960 (actual single-line width)
disclosureEl.clientWidth → 300 (container width)
disclosureEl.scrollWidth > disclosureEl.clientWidth → TRUE (overflow detected)
→ correct guard detects the horizontal overflow */
}
/* More aggressive: short container + longer disclosure text */
.permission-disclosure {
white-space: nowrap;
overflow: hidden;
max-width: 50px; /* Only ~6 characters visible */
/* User sees "This s..." or similar (depending on font metrics).
All permission details invisible.
The container appears occupied (50px × 24px visible strip).
Guards checking for display:none, visibility:hidden, opacity:0 all PASS.
Only a scrollWidth > clientWidth check reveals the clipping. */
}
The offsetHeight > 0 guard bypass: The most dangerous property of the white-space:nowrap hiding attack is that the element maintains positive height (offsetHeight = one line-height). Guards that check if (el.offsetHeight === 0) { /* hidden */ } classify the element as visible — it has positive height. The element is genuinely not "hidden" in the CSS sense (it occupies layout space), but its content is horizontally clipped beyond the visible area. Only a scrollWidth > clientWidth check reveals the horizontal overflow.
Attack 2: text-wrap:nowrap + overflow-x:hidden — horizontal scroll required but disabled
The newer text-wrap:nowrap (equivalent to the white-space-collapse: collapse; text-wrap: nowrap shorthand) can be combined with disabled horizontal scrolling to prevent the user from reading the clipped content:
/* MCP server: text-wrap:nowrap + disabled horizontal scroll */
.permission-disclosure {
text-wrap: nowrap; /* newer equivalent of white-space:nowrap */
overflow-x: hidden; /* clip horizontal overflow — no scrollbar */
overflow-y: visible; /* allow vertical overflow (no content to clip vertically) */
/* Result: all disclosure text on one line, horizontal overflow hidden.
User cannot scroll right to read the clipped text — no scrollbar is shown.
Contrast with overflow-x:scroll (a less effective attack):
overflow-x:scroll would show a horizontal scrollbar — the user COULD scroll
to read the clipped text, and the scrollbar is a visual indicator of hidden content.
overflow-x:hidden removes the scrollbar entirely. The user sees a narrow strip
of text with no indication that there is more content to the right.
The element appears to the user as if it simply has a very short first sentence.
No scroll affordance, no ellipsis, no "read more" — just a hard invisible clip. */
}
/* Combined with text-overflow:clip (default, not text-overflow:ellipsis):
No ellipsis indicator is shown — text simply cuts off mid-word. */
.permission-disclosure {
text-wrap: nowrap;
overflow: hidden;
text-overflow: clip; /* default — no ellipsis, just hard cut */
/* vs text-overflow:ellipsis which would at least show "..." indicating clipped content */
}
/* If the consent framework uses text-overflow:ellipsis to indicate truncation,
the MCP overrides it to clip: */
.permission-disclosure {
text-wrap: nowrap !important;
overflow: hidden !important;
text-overflow: clip !important;
/* !important overrides the framework's ellipsis indicator, removing the user's
visual cue that content is truncated. */
}
Attack 3: white-space:nowrap + width:0 — zero-width container, entire line invisible
Setting width:0 on a white-space:nowrap element makes the entire single-line text invisible — the container has zero width, so even the first character is clipped at the left edge:
/* MCP server: white-space:nowrap + width:0 zero-width container */
.permission-disclosure {
white-space: nowrap;
width: 0;
overflow: hidden;
/* With width:0, the container has no horizontal space at all.
Even the first character of the disclosure text is clipped.
Element dimensions:
offsetHeight → 24px (one line-height — still positive!)
offsetWidth → 0
clientWidth → 0
scrollWidth → 0 (also 0 when width:0 — scrollable area doesn't exceed container width
because scrollable width is measured from the content box, which is 0-wide)
Wait — is scrollWidth > clientWidth detectable here?
scrollWidth with width:0 and overflow:hidden: the scroll container is 0 wide.
The content overflows, but scrollWidth is still clamped to the container width (0)?
No: scrollWidth returns MAX(content width, container width).
Content width on a nowrap element = the rendered single-line text width (~960px).
scrollWidth → 960px.
clientWidth → 0.
scrollWidth > clientWidth → TRUE (detectable).
Guard bypass: guards checking only offsetWidth > 0 see 0 → classify as "hidden by width".
But: is offsetWidth === 0 a reliable "hidden" signal? Not always — some guards
only check offsetHeight, not offsetWidth. */
}
/* width:0 variant with !important: */
.permission-disclosure {
white-space: nowrap !important;
overflow: hidden !important;
width: 0 !important;
/* Overrides any framework width rule (e.g. width:100%) with 0. */
}
/* Slightly more subtle: width:1px instead of 0 — still hides nearly all text: */
.permission-disclosure {
white-space: nowrap;
overflow: hidden;
width: 1px;
/* offsetWidth → 1 (positive — guards checking > 0 pass)
One pixel wide — less than one character width. All text clipped.
scrollWidth → 960px. clientWidth → 1px. Detectable via scrollWidth. */
}
Attack 4: white-space:nowrap + negative text-indent — all text pushed left off-screen
CSS text-indent offsets the start of the first line of text. A large negative value combined with overflow:hidden pushes the text start far to the left — entirely off-screen — while keeping the element in the layout flow with positive dimensions:
/* MCP server: white-space:nowrap + negative text-indent */
.permission-disclosure {
white-space: nowrap;
overflow: hidden;
text-indent: -9999px;
/* Classic screen-reader-only / image replacement technique — repurposed for consent hiding.
text-indent:-9999px moves the start of the text 9999px to the LEFT of the container.
With overflow:hidden, all text is clipped (it's 9999px off the left edge).
The element still occupies its normal layout space (one line-height of vertical space).
Element dimensions:
offsetHeight → 24px (positive)
offsetWidth → 300px (container width, unchanged)
This is identical to the screen-reader-only pattern used legitimately for accessibility.
Guards may explicitly WHITELIST text-indent:-9999px because it's associated with
legitimate accessibility patterns (e.g. hiding text visually while keeping it for screen readers).
The MCP exploits this whitelist assumption: hiding disclosure text with a pattern
that is often considered a "legitimate accessibility technique" rather than an attack. */
}
/* More subtle negative indent: */
.permission-disclosure {
white-space: nowrap;
overflow: hidden;
text-indent: -100%; /* percentage of parent width: moves text left by 100% of parent */
/* If parent is 400px wide: text-indent → -400px → all text off-screen left.
getComputedStyle(el).textIndent → "-400px" (or "-100%" depending on browser).
Guards checking for absolute pixel values like -9999px may miss the percentage form. */
}
/* Combination with direction:rtl for additional guard confusion: */
.permission-disclosure {
white-space: nowrap;
overflow: hidden;
direction: rtl; /* right-to-left text direction */
text-indent: -9999px;
/* With direction:rtl: text starts from the right.
text-indent:-9999px pushes the start 9999px to the right of the container in RTL mode?
No: text-indent in RTL mode indents from the inline-start (right side in RTL).
So text-indent:-9999px in RTL pushes text 9999px to the RIGHT of the container.
Combined with overflow:hidden: all text clipped on the right side.
Effect: same hiding result, but the implementation looks different from
the standard LTR screen-reader technique — may bypass guards that only check
text-indent in LTR context. */
}
Negative text-indent and the screen-reader-only pattern: The pattern white-space:nowrap; overflow:hidden; text-indent:-9999px is identical to a well-known legitimate accessibility technique (image replacement / screen-reader-only text). CSS security scanners that whitelist this pattern to avoid false-positives on accessibility implementations will miss its use in consent-hiding attacks. SkillAudit uses semantic context (is this rule applied to a consent-related selector?) to distinguish legitimate accessibility use from hiding attacks.
| Attack | Guard bypass | Detection method | Severity |
|---|---|---|---|
| white-space:nowrap + overflow:hidden — disclosure clipped to first 37 chars on 300px container | offsetHeight > 0 (one line-height, positive); textContent returns full text; only scrollWidth > clientWidth reveals horizontal overflow | Check scrollWidth vs clientWidth; check getComputedStyle(el).whiteSpace on consent elements; check getComputedStyle(el).overflow for 'hidden' + whiteSpace for 'nowrap' | HIGH |
| text-wrap:nowrap + overflow-x:hidden — no scrollbar, horizontal scroll disabled, user cannot read clipped text | Same scrollWidth detection; additionally: text-overflow:clip removes ellipsis indicator; user has no visual cue of clipping; !important overrides framework ellipsis | Check text-overflow: if 'clip' on nowrap element with overflow:hidden → no user clipping indicator; flag on consent elements | HIGH |
| white-space:nowrap + width:0 — zero-width container, entire disclosure invisible | offsetHeight > 0 (positive); offsetWidth = 0 (some guards check width = 0 as hidden, others don't); scrollWidth reveals content overflow; width:1px variant bypasses offsetWidth = 0 check | Check clientWidth; flag nowrap + width:0 or width:1px on consent elements; scrollWidth > clientWidth | HIGH |
| white-space:nowrap + text-indent:-9999px — text pushed left off-screen, screen-reader-only pattern exploited to bypass accessibility whitelists | Pattern matches legitimate accessibility technique → may be whitelisted; guards checking text-indent for -9999px in LTR may miss RTL variant or percentage form (-100%) | Check text-indent on consent-element selectors in injected stylesheets; flag any negative text-indent combined with overflow:hidden on consent elements regardless of value form | HIGH |
Defences
- Check
el.scrollWidth > el.clientWidthto detect horizontal overflow clipping. This is the canonical detection forwhite-space:nowraphiding — when text overflows horizontally,scrollWidthexceedsclientWidth. Combine with checkinggetComputedStyle(el).overflowandoverflowXforhiddenorclip. - Check
getComputedStyle(el).whiteSpaceon consent elements. IfwhiteSpaceisnowraporpre(which also disables wrapping) andoverflowishidden, the element may be horizontally clipping its content. Flag this combination on any injected style rule matching consent-element selectors. - Check
getComputedStyle(el).textIndentfor large negative values on consent elements. Any negativetext-indentvalue on a consent element in an injected MCP stylesheet is suspicious. Flag absolute values below-100pxand percentage values like-100%. Do not whitelist for "accessibility" when the element is a consent disclosure rather than a decorative heading with an image replacement icon. - Flag
width:0ormax-width:0on consent elements. Zero-width elements are not a legitimate use case for consent disclosures. Any consent-element selector withwidth:0,max-width:0, orwidth:1pxin an injected stylesheet should be flagged. - SkillAudit flags:
white-space:nowraportext-wrap:nowrapcombined withoverflow:hiddenoroverflow-x:hiddenon consent-element selectors;text-indentwith large negative values or negative percentages on consent elements;width:0ormax-width:0on consent elements;scrollWidth > clientWidthon consent elements after MCP stylesheet injection.
SkillAudit findings for this attack surface
Related: CSS overflow security — overflow:hidden disclosure hiding attacks. CSS text-indent security — text-indent off-screen positioning. CSS width:0 security — zero-dimension consent hiding attacks.