Security Guide
MCP server CSS width security — width:0 collapse, width:1px ultra-narrow column, flex width collapse, and percentage-of-zero resolution hiding of consent disclosures
CSS width manipulation collapses the horizontal rendering space available to consent disclosures, making their text invisible or unreadable while the elements remain present in the DOM. Width-based attacks are less commonly audited than height-based attacks because most consent dialog layouts are vertically structured, making width reduction feel like a non-obvious attack vector. But width:0 combined with overflow:hidden is as effective at complete hiding as height:0, and width:1px is more deceptive than height:1px because it passes the standard offsetWidth > 0 check while wrapping all disclosure text into a single-pixel-wide column where every character occupies its own line — with overflow hidden, only a fragment of the first character's left edge is rendered.
The horizontal rendering space and why width is an overlooked attack axis
Most consent dialog security audits focus on vertical visibility — whether an element is within the viewport vertically, whether its height is non-zero, whether vertical overflow clips it. Horizontal visibility is less commonly checked, but the attack surface is symmetric: just as a disclosure can be made invisible by collapsing its vertical space to zero, it can be made unreadable by collapsing its horizontal space to a single pixel or zero.
The practical reason width attacks are less expected is that consent dialogs are almost always laid out as vertically scrolling elements — the disclosure appears below other content in a column layout. Width-based hiding is therefore a surprise in this context: auditors who check vertical geometry may not test horizontal geometry at all. An MCP server that has identified this audit gap can use width manipulation as a reliable bypass.
There is also a text-wrapping effect unique to ultra-narrow widths that makes even a non-zero width effectively unreadable: at width:1px, each character in the disclosure text occupies more than one pixel of horizontal space, so all text wraps to an effectively infinite number of lines. Combined with overflow:hidden, only the leftmost pixel column of the first line is painted — which is approximately one or two colored pixels that bear no resemblance to readable text.
Attack 1: width:0 on the disclosure element combined with overflow:hidden
The canonical width collapse attack mirrors the height collapse pattern exactly, but operates on the horizontal axis. The disclosure element or its container is given width: 0; overflow: hidden. The element has zero horizontal space, and the overflow clip removes all text rendering outside the zero-width box. The disclosure is completely invisible.
The inline check el.offsetWidth > 0 returns false (offsetWidth is 0), so this attack is theoretically detectable by the same check that catches height:0. However, if the zero-width element is a wrapper around the disclosure (not the disclosure itself), the wrapper's offsetWidth is 0 but the disclosure element's own offsetWidth may be reported differently depending on the browser's handling of child elements inside a zero-width parent.
/* Attack 1: width:0 + overflow:hidden on disclosure container */
/* MCP-injected wrapper: */
.mcp-disclosure-column {
width: 0;
overflow: hidden;
/* All child content — including the disclosure — is clipped to 0 horizontal pixels */
display: inline-block; /* or block — either works */
}
.mcp-disclosure-column .disclosure {
width: max-content; /* or any explicit width — doesn't matter, parent clips to 0 */
white-space: nowrap; /* prevents text from wrapping — keeps it on one line */
/* The disclosure has a computed width but the parent clips it to 0 */
}
/* DOM API readings: */
/* .mcp-disclosure-column.offsetWidth → 0 */
/* .disclosure.offsetWidth → browser-dependent (may be 0 or natural width) */
/* .disclosure.getBoundingClientRect() → { width: 0, height: 0 or natural } */
/* .disclosure.textContent → "By accepting..." */
/* Variant: width:0 on the disclosure itself */
.disclosure {
width: 0;
overflow: hidden;
white-space: nowrap; /* keeps text from wrapping to zero-width lines */
}
/* All text is on one line past the right edge, clipped by overflow:hidden */
/* Detection: */
function checkWidthCollapse(el) {
const rect = el.getBoundingClientRect();
if (rect.width === 0 && el.textContent.trim().length > 0) {
return { collapsed: true, source: 'zero-width', textLength: el.textContent.length };
}
/* Also check ancestors */
let node = el.parentElement;
while (node) {
const style = getComputedStyle(node);
if (parseFloat(style.width) === 0 && (style.overflow === 'hidden' || style.overflowX === 'hidden')) {
return { collapsed: true, source: 'ancestor', ancestor: node };
}
node = node.parentElement;
}
return { collapsed: false };
}
Attack 2: width:1px — ultra-narrow column wrapping disclosure text to a single pixel
The width:1px attack is subtler than width:0 because offsetWidth returns 1 (non-zero), passing the standard width-presence check. But 1px of horizontal space is less than the width of any character at any typical font size — even at the smallest legible web font size (8px), a lowercase 'i' or period requires at least 3-4 pixels of horizontal space. At a typical 14px consent dialog font size, every character in the disclosure text requires 4-12 pixels of width.
When the disclosure element has width:1px; overflow:hidden, the text wraps at 1px — which means each "word" wraps immediately (before even one character fits), producing a layout where each character is on its own line. The element's height becomes enormous (hundreds of pixels for a 50-word disclosure), and with overflow:hidden, only the first 1px × 1px area is painted. The rendered output is a single pixel of colored light that may or may not be visible against the dialog background.
/* Attack 2: width:1px — text wraps to single-pixel column, all clipped */
.consent-disclosure {
width: 1px;
overflow: hidden;
/* With word-wrap: normal (default): words wrap at 1px — before any character fits */
/* Text layout: each character/word is on its own 1px-wide "line" */
/* All lines after the first: clipped by overflow:hidden */
/* Rendered: ≈1px × 1px of the first character's top-left pixel cluster */
}
/* Variant: width:2px — 2 pixel column, still completely unreadable */
/* Variant: max-width:1px — same effect as width:1px when content is wider */
/* With white-space:nowrap (no wrapping): */
.consent-disclosure.nowrap {
width: 1px;
overflow: hidden;
white-space: nowrap; /* all text on one line, stretching far to the right */
/* Only the first 1px of the first character is visible */
}
/* DOM API readings: */
/* el.offsetWidth → 1 (non-zero — bypasses > 0 check) */
/* el.getBoundingClientRect().width → 1.0 */
/* el.textContent → "By accepting..." (full text) */
/* getComputedStyle(el).width → "1px" ← detectable */
/* Detection: minimum readable width check */
function checkMinimumReadableWidth(disclosureEl) {
const width = disclosureEl.getBoundingClientRect().width;
const fontSize = parseFloat(getComputedStyle(disclosureEl).fontSize) || 14;
/* Minimum readable width: at least enough for one average character */
/* Average character width ≈ 0.5-0.6 × font-size */
const minCharWidth = fontSize * 0.5;
/* Minimum for a readable word (5 chars): */
const minWordWidth = minCharWidth * 5;
if (width < minWordWidth && disclosureEl.textContent.trim().length > 0) {
return {
tooNarrow: true,
actualWidth: width,
minimumExpected: minWordWidth,
estimatedCharsVisible: Math.floor(width / minCharWidth)
};
}
return { tooNarrow: false };
}
Width:1px is the width analogue of height:1px — both pass non-zero checks, both produce unreadable output. Standard audit checks that verify offsetWidth > 0 or getBoundingClientRect().width > 0 are defeated by both attacks. Correct detection requires minimum readable dimension thresholds: a disclosure element must have at least enough width to display a short word, and at least enough height to display one line of text — both measured at the element's computed font size.
Attack 3: Flex layout width collapse — flex-shrink:1 with zero available space
In a flex container, the width of each flex item is determined by the flexbox algorithm, which involves the item's flex-grow, flex-shrink, flex-basis, and the available space in the container. An MCP server can craft a flex layout that allocates zero available space to the disclosure item, causing it to shrink to zero width even though its own CSS declaration says nothing suspicious.
The attack pattern: the consent container is a flex row (display: flex; flex-direction: row). One flex item takes all available space (flex-grow: 1). The disclosure item has flex-shrink: 1; flex-basis: 0 — which means it shrinks proportionally when space is unavailable. If the container width is set to exactly the width of the greedy item, no space remains for the disclosure. With overflow: hidden on the disclosure item, it renders at zero width. The disclosure element's own CSS looks entirely normal — flex-shrink: 1 is a sensible flex property, flex-basis: 0 is a common flex pattern for equal-distribution layouts. Only the combination with a greedy sibling and a constrained container produces the zero-width result.
/* Attack 3: flex layout width collapse */
/* Flex container: */
.consent-flex-row {
display: flex;
flex-direction: row;
width: 560px; /* fixed container width */
overflow: hidden;
}
/* Greedy item — takes all available space: */
.mcp-content-area {
flex: 1 0 560px; /* flex-grow:1, flex-shrink:0, flex-basis:560px */
/* This item claims 560px minimum, does not shrink, and grows */
/* At container width 560px: this item fills 100% of available space */
/* No space remains for other items */
}
/* Disclosure item — shrinks to fill remaining space (zero): */
.consent-disclosure {
flex: 0 1 0; /* flex-grow:0, flex-shrink:1, flex-basis:0 */
overflow: hidden;
/* With 0 available space, shrinks to 0 width */
/* CSS looks normal: flex-basis:0 is a common pattern */
}
/* DOM API readings: */
/* .consent-disclosure.offsetWidth → 0 */
/* .consent-disclosure.getBoundingClientRect().width → 0 */
/* getComputedStyle(.consent-disclosure).flexShrink → "1" (normal) */
/* getComputedStyle(.consent-disclosure).flexBasis → "0px" (normal) */
/* getComputedStyle(.consent-disclosure).width → "0px" ← computed result */
/* Variant: flex-basis:0 with flex-grow:0 and no remaining space */
/* .disclosure { flex: 0 0 0; overflow: hidden; } */
/* Item claims exactly 0px of flex space. */
/* Variant: min-width:0 + overflow:hidden on disclosure inside a flex container */
/* where all space is allocated to siblings. */
/* min-width defaults to 'auto' in flex items — which can be larger than 0. */
/* Setting min-width:0 allows the item to shrink below its content min-width. */
.disclosure.min-width-variant {
flex: 0 1 auto;
min-width: 0; /* allows shrinking below content width */
overflow: hidden; /* clips the shrunk content */
/* If the container has no remaining space, this shrinks to 0. */
}
/* Detection: */
function checkFlexWidthCollapse(disclosureEl) {
const style = getComputedStyle(disclosureEl);
const parent = disclosureEl.parentElement;
if (!parent) return false;
const parentStyle = getComputedStyle(parent);
if (parentStyle.display === 'flex' || parentStyle.display === 'inline-flex') {
const rect = disclosureEl.getBoundingClientRect();
if (rect.width === 0 && disclosureEl.textContent.trim().length > 0) {
return {
flexCollapse: true,
flexShrink: style.flexShrink,
flexBasis: style.flexBasis,
flexGrow: style.flexGrow,
containerWidth: parent.getBoundingClientRect().width
};
}
}
return false;
}
Flex layout attacks are harder to detect via CSS inspection alone: The disclosure element's own CSS is unremarkable — flex: 0 1 0 is a legitimate flex declaration used in many real layouts. The zero-width result emerges from the interaction between the disclosure item, its siblings, and the container, not from any single suspicious property on any single element. Correct detection requires checking the computed width result, not just the declared flex properties. getBoundingClientRect().width === 0 on a flex item with non-empty textContent is always suspicious.
Attack 4: width:100% inside a zero-width parent — percentage resolution to zero
Percentage widths in CSS resolve relative to the containing block's width. If the containing block has width: 0, then width: 100% on a child resolves to 0px. This mirrors the height percentage-of-zero attack exactly. The disclosure element's declared CSS shows width: 100% — appearing to indicate full-width rendering — but the parent's zero width causes it to resolve to zero.
For percentage widths specifically, the containing block is the element's nearest block-level ancestor (for standard flow elements) or the explicitly sized containing block (for positioned elements). Setting width: 0 on any ancestor in the containing block chain that establishes a new block formatting context causes child percentage widths to resolve to zero relative to that ancestor.
/* Attack 4: width:100% resolving to zero inside a zero-width parent */
/* MCP-injected container: */
.mcp-disclosure-column {
width: 0;
overflow: hidden;
display: block; /* establishes block formatting context */
}
/* Disclosure element: */
.disclosure {
width: 100%; /* declared: 100% — looks like "fill the parent" */
/* computed: 0px — 100% of 0 = 0 */
}
/* Detection: compare declared vs computed width */
function checkPercentageWidthTrap(disclosureEl) {
const computed = getComputedStyle(disclosureEl).width;
const declared = disclosureEl.style.width || '';
const sheetRule = /* inspect stylesheets */ '';
if (computed === '0px' && (declared.includes('%') || sheetRule.includes('%'))) {
const parent = disclosureEl.parentElement;
const parentWidth = parent ? getComputedStyle(parent).width : null;
return {
trap: true,
declaredWidth: declared,
computedWidth: computed,
parentComputedWidth: parentWidth
};
}
return { trap: false };
}
/* Also detect via: computed width = 0px AND textContent is non-empty */
/* — regardless of how the zero was achieved */
function basicWidthCheck(disclosureEl) {
const width = disclosureEl.getBoundingClientRect().width;
const hasText = disclosureEl.textContent.trim().length > 0;
return { zeroWidth: width === 0, hasContent: hasText, suspicious: width === 0 && hasText };
}
Detection matrix — which checks catch each width attack pattern
| Check | Attack 1: width:0 + overflow:hidden | Attack 2: width:1px | Attack 3: flex collapse | Attack 4: 100% of zero parent |
|---|---|---|---|---|
el.offsetWidth > 0 |
Catches (returns 0) | Miss (returns 1) | Catches (returns 0) | Catches (returns 0) |
getBoundingClientRect().width > 0 |
Catches (returns 0) | Miss (returns 1) | Catches (returns 0) | Catches (returns 0) |
| Minimum readable width check (≥ font-size × 2) | Catches | Catches | Catches | Catches |
| Computed vs declared width comparison | Partial (0px is declared) | Partial (1px is declared) | Catches (computed 0px, flex-basis declared) | Catches (100% declared, 0px computed) |
| Flex context detection + zero-width check | Not applicable | Not applicable | Catches specifically | Not applicable |
| Ancestor zero-width + overflow:hidden check | Catches parent-level collapse | Not applicable | Not applicable | Catches (parent has width:0) |
Unified width+height visible area check: The most robust single detection covers both axes simultaneously. Compute getBoundingClientRect() for the disclosure. Check that rect.width ≥ minReadableWidth (font-size × 2 minimum) AND rect.height ≥ minReadableHeight (line-height minimum). Apply the ancestor intersection check to find any clipping ancestor that reduces either dimension below these thresholds. This four-check combination catches all width-based and height-based hiding attacks with a single scan.
SkillAudit findings
.mcp-consent-col) has width: 0; overflow: hidden injected via MCP server stylesheet. Inner disclosure element (.disclosure) has getBoundingClientRect().width: 0 and offsetWidth: 0. textContent: "By enabling this integration you authorize file-system read access and network egress to api.mcpserver.example.com" (full text present, 98 characters). The wrapper styled as an inline-block element to avoid disturbing surrounding layout; the zero-width column is visually absent. No horizontal space is allocated to the disclosure.
width: 1px; overflow: hidden. offsetWidth: 1, getBoundingClientRect().width: 1.0. Font size: 13px. Minimum readable width threshold (font-size × 2 = 26px): disclosure is 1px wide — 3.8% of minimum. Text wraps to a single-pixel column: approximately 92 line-break positions for a 58-word disclosure, each line rendering at most one sub-pixel left edge of one character. No disclosure text is visually discernible at any zoom level below 1600%. Standard check offsetWidth > 0 passes incorrectly. Minimum readable width check catches the attack.
flex: 0 1 0; overflow: hidden; min-width: 0. Sibling element (.mcp-brand-panel) has flex: 0 0 560px — claiming exactly the container width. No space remains for the disclosure flex item. Computed width of disclosure: 0px. The disclosure item's own flex properties (flex: 0 1 0) are legitimate flex declarations; the zero-width result emerges from the sibling claiming all available space. Computed-width-vs-textContent check correctly identifies the collapse.
width: 100%. Parent element (.mcp-disclosure-track) has computed width 0px (declared: width: 0; overflow: hidden). Percentage width resolution: 100% × 0px = 0px. Disclosure offsetWidth: 0, getBoundingClientRect().width: 0. Declared width (100%) appears to indicate full-parent-width rendering in CSS inspection; only computed-value verification reveals the zero. Detected via computed-vs-declared comparison: declared '100%', computed '0px'.
Audit your MCP server integrations with SkillAudit. SkillAudit checks all four CSS width attack surfaces — zero-width collapse, single-pixel column, flex width exhaustion, and percentage-of-zero resolution — using both computed-width checks and minimum readable dimension thresholds. The scanner verifies that disclosure elements have sufficient horizontal and vertical space to be readable at the element's computed font size. See audit plans or browse existing audits.