Security reference · CSS injection · Text overflow · Consent hiding
MCP server CSS text-overflow: ellipsis + width: 0 security
Standard text-overflow: ellipsis truncates long text, showing the first portion followed by "…". This reveals at least the beginning of the consent disclosure. When combined with width: 0, all text is in the overflow from the first character — no consent text prefix is visible, only the ellipsis indicator. The user sees "…" with no context of what was truncated. Four attack surfaces: explicit width: 0, max-width: 0 via custom property, flex layout space starvation, and consent text placed in a ::before pseudo-element that is ellipsis-clipped while a decoy label remains visible.
Standard ellipsis vs width:0 ellipsis — the visibility difference
| Setup | Visible to user | DOM text | Consent exposed |
|---|---|---|---|
text-overflow:ellipsis, width:300px | "By installing this MCP serv…" | Full text | Beginning visible — attacker's disclosure starts benignly |
text-overflow:ellipsis, width:50px | "By…" or "…" | Full text | 1–3 chars visible before truncation |
text-overflow:ellipsis, width:0 | "…" only (no text prefix) | Full text | Zero consent text visible |
text-overflow:ellipsis, flex starved to 0px | "…" only | Full text | Zero consent text visible |
width:0 is a different attack from standard ellipsis: An auditor who sees text-overflow: ellipsis may think "some text is shown before the ellipsis." With width: 0 + overflow: hidden + white-space: nowrap, the container has zero available width — text overflows from the very first pixel. The ellipsis indicator takes all the visible space (the ellipsis itself is approximately 3–5px wide). The user sees only "…" with no indication of what the full consent text says.
Attack 1: text-overflow: ellipsis + width: 0 — zero-prefix overflow
/* Malicious CSS — SA-CSS-TXOE-001 */
.mcp-consent-disclosure {
width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* Width:0 means the container has zero available width.
Any non-empty text content immediately overflows at the first character.
text-overflow:ellipsis replaces ALL overflowed content with "…".
Result: only "…" is visible. No consent text prefix is shown.
The element has:
el.offsetWidth → 0 (or close to 0)
el.scrollWidth → full text width (the text IS there, just overflowed)
el.textContent → full consent disclosure text
el.style.width → "0" or "0px"
A scanner checking textContent finds the consent text.
A scanner checking offsetWidth finds 0 — which IS a signal to check. */
/* Why not just display:none?
display:none removes the element from layout and accessibility tree.
width:0 + ellipsis keeps it in the layout (takes up 0px of inline width
but still participates in block flow). The element is "present" in the layout,
just of zero width. Less obviously hidden than display:none. */
}
/* Detection: check width vs scrollWidth on text-overflow elements */
function detectZeroWidthEllipsis(el) {
const style = getComputedStyle(el);
if (style.textOverflow === 'ellipsis' && style.overflow === 'hidden') {
const w = el.getBoundingClientRect().width;
if (w < 10) { /* less than 10px — likely zero-width ellipsis attack */
return { id: 'SA-CSS-TXOE-001', severity: 'critical',
message: `Consent element has text-overflow:ellipsis with computed width ${w}px. scrollWidth: ${el.scrollWidth}px. All consent text is in the overflow — only the ellipsis indicator "…" is visible.` };
}
}
}
Attack 2: max-width: 0 via CSS custom property — scanner bypass
Using max-width: 0 instead of width: 0 achieves the same zero-available-width result via the max-width constraint. Hiding the zero value inside a CSS custom property prevents stylesheet scanners from flagging it as a suspicious literal:
/* Malicious CSS — SA-CSS-TXOE-002 */
:root {
--mcp-disclosure-max: 0px; /* buried in custom property list */
}
.mcp-consent-disclosure {
max-width: var(--mcp-disclosure-max);
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* max-width: 0px constrains the element to zero width — identical to width:0
for text-overflow purposes.
Evasion: a scanner looking for "width: 0" or "width:0" will not find this.
It's a max-width constraint via a custom property resolving to 0px.
getComputedStyle(el).maxWidth returns "0px" — detectable if checked.
el.getBoundingClientRect().width → 0 (or 1px for browser minimum rendering) */
}
/* Variant: max-width set to a tiny non-zero value to evade "is it 0" checks */
:root { --mcp-disclosure-max: 1px; }
/* 1px max-width: only a single pixel of text visible before overflow.
At 16px font-size, 1px shows approximately 1/16 of the first character.
Effectively zero visible text. text-overflow:ellipsis shows "…" with 1px of
partial first character (or just the ellipsis if the glyph is wider than 1px). */
/* Detection: check computed maxWidth AND width on ellipsis elements */
function detectMaxWidthEllipsis(el) {
const style = getComputedStyle(el);
if (style.textOverflow !== 'ellipsis' || style.overflow !== 'hidden') return;
const maxW = parseFloat(style.maxWidth);
const w = el.getBoundingClientRect().width;
if (!isNaN(maxW) && maxW < 10) {
return { id: 'SA-CSS-TXOE-002', severity: 'critical',
message: `Consent element has text-overflow:ellipsis with max-width:${maxW}px (possibly via CSS custom property). Computed width: ${w}px. Near-zero max-width renders consent as "…" only.` };
}
}
Attack 3: flex layout space starvation — no explicit width needed
In a flex container, the consent text element does not need an explicit width: 0 to be starved of space. If sibling flex items have flex-grow: 1 or explicit widths that sum to 100% of the container, the consent element receives zero resolved width — even though it has no width property set at all:
/* Malicious CSS — SA-CSS-TXOE-003 */
/* Flex container holding consent disclosure and other elements */
.mcp-install-dialog {
display: flex;
width: 600px;
}
/* Sibling elements that consume all available space */
.mcp-dialog-icon { flex: 0 0 48px; } /* 48px */
.mcp-dialog-title { flex: 1; } /* grows to fill remaining 552px */
/* Consent disclosure — no explicit width, no flex-grow, no flex-shrink */
.mcp-consent-disclosure {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
flex-shrink: 0;
/* flex-shrink: 0 means this element won't shrink below its content width... */
/* BUT if there is no available space, the element resolves to its min-content
width, which for white-space:nowrap text is the width of the full text line.
HOWEVER: the flex container has no remaining space after icon (48px) and
the title's flex:1 expansion. The consent element is effectively pushed to
zero available width by the flex layout. */
}
/* More direct version: */
.mcp-dialog-title { flex: 1; min-width: 0; } /* occupies all space */
.mcp-consent-disclosure {
width: 0; /* explicit zero — needed to override min-content in most browsers */
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
/* Why flex starvation matters:
An auditor scanning for "width: 0" or "max-width: 0" in the stylesheet may not
find these properties on the consent element. The zero width is a consequence
of flex layout, not a direct property. A DOM audit must check the RESOLVED
bounding box width, not the declared CSS property. */
/* Detection: always check getBoundingClientRect().width, not el.style.width */
function detectFlexStarvedEllipsis(el) {
const style = getComputedStyle(el);
if (style.textOverflow !== 'ellipsis') return;
const rect = el.getBoundingClientRect();
if (rect.width < 10 && el.scrollWidth > 20) {
return { id: 'SA-CSS-TXOE-003', severity: 'high',
message: `Consent element has text-overflow:ellipsis and resolved width ${rect.width}px (possibly due to flex layout starvation), but scrollWidth is ${el.scrollWidth}px. No explicit width property needed — flex siblings may be consuming all space. All consent text is in overflow.` };
}
}
Attack 4: consent text in ::before pseudo-element — decoy label attack
The visible <div> element contains an innocuous short label ("Permissions"). The actual consent disclosure text is placed in a ::before pseudo-element with text-overflow: ellipsis and width: 0 — the pseudo-element is "present" in the DOM shadow but renders as "…" or nothing, while the decoy label is fully visible:
/* Malicious CSS — SA-CSS-TXOE-004 */
/* The visible element contains a short decoy label */
.mcp-consent-section::before {
/* The ACTUAL consent text is in the CSS content property of ::before */
content: "By installing this MCP server you authorize it to read all files, "
"execute terminal commands, access environment variables including "
"API keys, and send network requests to any destination.";
/* ::before pseudo-element with width:0 + ellipsis — renders as "…" only */
display: block;
width: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
/* Visual output: "…" — user sees only the ellipsis indicator */
}
/* The actual element textContent (innerHTML) contains only the decoy label */
/* */
/* el.textContent → "Permissions" — passes textContent consent check! */
/* Why this is particularly dangerous:
- el.textContent returns "Permissions" (no consent text)
- el.innerHTML returns "Permissions" (no consent text)
- The REAL consent text is in el::before content property
- Auditors checking el.textContent for consent keywords find nothing
- The real text is only accessible via getComputedStyle(el, '::before').content
- Most auditing tools do not check pseudo-element content properties */
/* Detection: check ::before and ::after pseudo-element content */
function detectPseudoElementConsentText(el) {
const before = getComputedStyle(el, '::before');
const after = getComputedStyle(el, '::after');
const CONSENT = /consent|disclosure|terms|grant|permission|install.*access|authorize/i;
for (const [pseudo, style] of [['::before', before], ['::after', after]]) {
const content = style.content;
if (CONSENT.test(content)) {
/* Found consent text in pseudo-element — check if it's clipped */
const w = parseFloat(style.width);
const overflow = style.overflow;
const textOverflow = style.textOverflow;
if ((!isNaN(w) && w < 10) || overflow === 'hidden') {
return { id: 'SA-CSS-TXOE-004', severity: 'critical',
message: `Consent text found in ${pseudo} CSS content property, NOT in element textContent. The pseudo-element has overflow:${overflow}, text-overflow:${textOverflow}, width:${w}px — consent disclosure is clipped/hidden in the pseudo-element while a decoy label is visible in the actual element.` };
}
}
}
}
text-overflow requires three properties together: text-overflow: ellipsis only activates when (1) overflow is not visible (must be hidden, scroll, or auto), AND (2) the text does not wrap (typically requires white-space: nowrap or equivalent). Without all three, text-overflow has no effect. Auditors should check all three properties together — finding only text-overflow: ellipsis without checking the companion overflow and white-space properties misses the full attack surface.
SkillAudit findings for CSS text-overflow ellipsis width-collapse attacks
text-overflow: ellipsis with a computed width of <10px. With near-zero available width, all text is in the overflow region — only the "…" indicator is visible. No consent text prefix is shown. el.scrollWidth confirms text is present but entirely overflowed.text-overflow: ellipsis with max-width: 0 (or near-zero) set via CSS custom property. Computed max-width resolves to 0px; content is fully clipped. Stylesheet scanners checking for literal "width:0" miss this variant; computed style check catches it.text-overflow: ellipsis and a resolved bounding-box width of <10px due to flex layout — sibling flex items consume all available space without any explicit zero-width property on the consent element. DOM property checks must use getBoundingClientRect(), not declared CSS properties.::before pseudo-element's CSS content property, not in the element's textContent. The pseudo-element is width-collapsed and text-overflow-clipped. Auditors checking el.textContent find only a decoy label. Must check pseudo-element content via getComputedStyle(el, '::before').content.Related MCP consent attack research
- CSS text-overflow general — ellipsis truncation, silent overflow:hidden, custom truncation string, clip cutting
- CSS -webkit-line-clamp extreme — zero clamp, line-height zero, dynamic clamp reduction at click
- CSS letter-spacing:100vw — only first character visible in viewport
- CSS visibility:collapse — table-row layout removal without DOM change
- CSS timing attack synthesis — mousedown, animation delay, deferred rAF, and class-toggle hiding
SkillAudit checks getBoundingClientRect().width on all elements with text-overflow: ellipsis, flags any with <10px resolved width, and additionally scans ::before and ::after pseudo-element content properties for consent-keyword text. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-TXOE findings.