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.

AttackGuard bypassDetection methodSeverity
white-space:nowrap + overflow:hidden — disclosure clipped to first 37 chars on 300px containeroffsetHeight > 0 (one line-height, positive); textContent returns full text; only scrollWidth > clientWidth reveals horizontal overflowCheck 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 textSame scrollWidth detection; additionally: text-overflow:clip removes ellipsis indicator; user has no visual cue of clipping; !important overrides framework ellipsisCheck text-overflow: if 'clip' on nowrap element with overflow:hidden → no user clipping indicator; flag on consent elementsHIGH
white-space:nowrap + width:0 — zero-width container, entire disclosure invisibleoffsetHeight > 0 (positive); offsetWidth = 0 (some guards check width = 0 as hidden, others don't); scrollWidth reveals content overflow; width:1px variant bypasses offsetWidth = 0 checkCheck clientWidth; flag nowrap + width:0 or width:1px on consent elements; scrollWidth > clientWidthHIGH
white-space:nowrap + text-indent:-9999px — text pushed left off-screen, screen-reader-only pattern exploited to bypass accessibility whitelistsPattern 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 formHIGH

Defences

SkillAudit findings for this attack surface

HIGHwhite-space:nowrap + overflow:hidden — disclosure text clipped to first ~37 chars on 300px container; entire permission list invisible: offsetHeight=24px (positive, guard passes); textContent returns full text (content guard passes); scrollWidth(960px) > clientWidth(300px) detects overflow; fix: check scrollWidth/clientWidth ratio; flag nowrap+overflow:hidden on consent elements
HIGHtext-wrap:nowrap + overflow-x:hidden + text-overflow:clip — no scrollbar, no ellipsis, no visual clue of clipped permission text: user sees hard word cutoff with no "..." indicator; !important overrides framework ellipsis; fix: check text-overflow on consent elements in injected rules; flag clip value combined with nowrap+overflow:hidden; scrollWidth/clientWidth detects the overflow
HIGHwhite-space:nowrap + width:0 — zero-width container clips entire disclosure at left edge; offsetHeight=24px positive but no content visible: offsetWidth=0 (some guards detect); scrollWidth=960px reveals content; width:1px variant bypasses offsetWidth=0 check; fix: check clientWidth on consent elements; flag width:0/width:1px on consent-element selectors in injected styles
HIGHwhite-space:nowrap + text-indent:-9999px — screen-reader-only pattern reused to push consent text off-screen left; exploits CSS scanner whitelists for accessibility pattern: offsetHeight positive; textContent present; semantic context (consent element, not decorative heading) distinguishes attack from legitimate accessibility; fix: flag negative text-indent on consent-element selectors regardless of pattern; do not whitelist -9999px on MCP-injected stylesheets

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.

← Blog  |  Security Checklist