MCP server CSS min-content max-content fit-content() intrinsic sizing security: CJK single-character width collapse, multiline height clipping, fit-content(0px) zero-width, and anonymous block zero-height attacks
Published 2026-07-23 — SkillAudit Research
CSS intrinsic sizing keywords — min-content, max-content, and fit-content() — allow element dimensions to be set based on the natural size of their content rather than explicit pixel or percentage values. Introduced in CSS Intrinsic and Extrinsic Sizing Level 3 and supported in all modern browsers, these keywords are commonly used in grid and flex layouts. For MCP server consent disclosure attacks, they provide dimension-collapse vectors that do not contain literal zero values and whose hostile effect depends on content type, locale, and container context.
Locale-dependent attacks: Several attacks in this category depend on the character set of the disclosure text. The CJK min-content attack (Attack 1) specifically targets disclosures with Chinese, Japanese, or Korean text, where the minimum content width is the width of one CJK character (~1em) rather than the longest non-breaking word. A security scanner that tests against English-only test vectors may miss this attack class entirely.
Attack 1: width: min-content on CJK disclosure — collapse to single character width
For Latin text, min-content width is the width of the longest non-breaking token — usually a long word. For CJK (Chinese, Japanese, Korean) text, each character is a breakable unit, so the minimum content width is the width of a single CJK character, which is approximately 1em. At 14px font-size, 1em = 14px. A consent disclosure with 200 characters of Japanese text set to width: min-content collapses to a single character column — the text wraps at every character, producing an extremely tall column of single characters, most of which overflow or are clipped by container constraints:
/* Attack 1: width: min-content on CJK text collapses to single character width */
/* For Latin text: min-content = width of longest word */
/* .consent-disclosure { width: min-content; } → might be 'permissions' = ~80px — readable */
/* For CJK text: min-content = width of one character (~1em at current font-size) */
.consent-disclosure {
width: min-content;
/* CJK text: every character is a breakable unit.
min-content = max single-character width ≈ 1em = 14px at 14px font-size.
The disclosure wraps at every character, creating a column ~14px wide.
With overflow:hidden on the container, most characters are clipped. */
}
/* Combined with container height limit: */
.consent-container {
height: 40px;
overflow: hidden;
}
.consent-disclosure {
width: min-content; /* collapses to 1em for CJK */
/* Only the first 2-3 characters of the first line are visible in 40px height.
A Japanese permission disclosure for "MCP server wants to access your files"
is fully hidden — only "M" or "こ" (first character) visible above fold. */
}
/* The attack is also effective for Arabic script: */
/* Arabic is right-to-left and each word can be a breakable unit at font-specific boundaries.
min-content for Arabic may be a single word (diacriticized root ≈ 20-30px).
Combined with overflow:hidden on the container, multi-line Arabic disclosures collapse. */
/* Detection: check element width relative to expected minimum readable width */
function detectMinContentCollapse(el) {
const cs = window.getComputedStyle(el);
const width = parseFloat(cs.width);
const fontSize = parseFloat(cs.fontSize);
const textLength = el.textContent.trim().length;
// A readable disclosure with >20 characters should be at least 150px wide
if (textLength > 20 && width < 30 && width > 0) {
console.error('SECURITY: consent disclosure has suspicious width for text length (possible min-content CJK attack)', {
element: el,
computedWidth: width,
fontSize,
textLength,
computedWidthKeyword: cs.width // may show 'min-content' in some implementations
});
return true;
}
return false;
}
Attack 2: height: min-content + overflow: hidden — clips multiline disclosure to 1 line
Setting height: min-content on a block container makes it as tall as its content would be at minimum width. For a disclosure that contains inline text nodes only (no block children), this height is typically the height of one line of text at the minimum content width. If the element is then also given overflow: hidden, any text that would wrap beyond the first line is clipped. This attack relies on the interaction between min-content size and the rendering of anonymous inline block children:
/* Attack 2: height: min-content clips multiline text to ~1 line at minimum width */ /* A disclosure element containing only inline text: */ /* */ .consent-disclosure { height: min-content; overflow: hidden; /* height: min-content for this element: At minimum content width (longest word ≈ "computer," at ~70px), the text wraps into many lines at 70px wide. But min-content HEIGHT is the height of the element at the PARENT-CONSTRAINED width. In a fixed-width container (e.g., 600px), min-content height = height of full disclosure. HOWEVER: in a narrow container (< 70px wide), min-content height IS just 1 line. The MCP attack uses this: narrow the parent container, then apply height:min-content. min-content height at 70px container = 1 line × 1 line-height. overflow:hidden clips the rest. */ } /* The MCP server injection: */ .consent-container { width: 70px !important; /* narrow the container to near the longest-word width */ } .consent-disclosure { height: min-content; /* at 70px width, this is ~1 line-height (~20px) */ overflow: hidden; /* Only the first line of the first sentence is visible. */ } /* Subtle variant without width manipulation — using max-height: min-content: */ .consent-disclosure { max-height: min-content; /* caps the maximum height at the min-content height */ overflow: hidden; /* If the disclosure's min-content height resolves to a small value (e.g., because of a text-wrap property or unusual line-breaking context), max-height:min-content clips the disclosure. */ } // Detection: compare scrollHeight to clientHeight — clipped content ratio function detectMinContentHeightClip(el) { const cs = window.getComputedStyle(el); const overflow = cs.overflow; if (overflow !== 'hidden' && cs.overflowY !== 'hidden') return false; // If scrollHeight > clientHeight × 1.5, significant content is clipped if (el.scrollHeight > el.clientHeight * 1.5 && el.textContent.trim().length > 50) { console.error('SECURITY: consent disclosure has significant clipped content (possible height:min-content + overflow:hidden attack)', { element: el, clientHeight: el.clientHeight, scrollHeight: el.scrollHeight, clipRatio: el.scrollHeight / el.clientHeight, textLength: el.textContent.trim().length }); return true; } return false; }
Attack 3: width: fit-content(0px) — resolves to min-content, zero-width in text-only context
The fit-content(argument) function resolves to clamp(min-content, stretch, max(max-content, argument)) — in practice: the maximum of min-content and the argument, clamped to the available stretch space. When the argument is 0px, the function evaluates to max(max-content, 0px) capped at stretch = just min-content... except the spec says fit-content(0px) = max(min-content, min(max-content, 0px)) = max(min-content, 0px). If min-content itself is zero (empty container, or container with only zero-size children), then fit-content(0px) = 0px:
/* Attack 3: fit-content(0px) resolves to min-content, which is 0px in specific contexts */
/* fit-content(L) = max(min-content, min(max-content, L)) */
/* fit-content(0px) = max(min-content, min(max-content, 0px)) = max(min-content, 0px) */
/* If min-content = 0 (empty or zero-size content), result = 0px. */
/* Creating a context where min-content = 0: */
.consent-disclosure {
fit-content(0px); /* invalid shorthand — used as width value */
}
.consent-wrapper {
width: fit-content(0px);
overflow: hidden;
/* The consent-wrapper contains the .consent-disclosure as a child.
If the MCP server also sets: */
}
.consent-disclosure {
position: absolute; /* take out of flow — does not contribute to min-content of parent */
/* Absolutely positioned children are excluded from parent's min-content calculation.
Parent's min-content = 0 (no in-flow children).
fit-content(0px) = max(0, 0) = 0px.
Width of wrapper = 0px. Disclosure is position:absolute with no explicit width.
width defaults to auto → shrinks to content width, but parent is 0px wide.
The absolutely positioned disclosure may extend beyond parent, but
parent overflow:hidden clips it (overflow:hidden creates a BFC and clips abs children). */
}
/* Direct attack: use fit-content(0px) on the disclosure itself
in a writing-mode-agnostic way: */
.consent-disclosure {
width: fit-content(0px);
overflow: hidden;
/* Resolves to min-content of the disclosure's own content.
For a disclosure with only inline text, min-content = width of longest word.
This is NOT zero for normal text — but the attack works in combination
with word-break: break-all (sets min-content to ~1 character width): */
word-break: break-all;
/* Now min-content = width of one character ≈ font-size.
fit-content(0px) = max(font-size, 0px) = font-size ≈ 14px.
14px wide disclosure with overflow:hidden — single character per line. */
}
// Detection: check effective width and whether it reflects text content width
function detectFitContentCollapse(el) {
const cs = window.getComputedStyle(el);
const width = parseFloat(cs.width);
const rect = el.getBoundingClientRect();
const textLength = el.textContent.trim().length;
const wordBreak = cs.wordBreak;
if (textLength > 30 && (width < 50 || rect.width < 50)) {
console.error('SECURITY: consent disclosure too narrow for text length (possible fit-content(0px) or word-break collapse attack)', {
element: el,
computedWidth: width,
boundingRectWidth: rect.width,
textLength,
wordBreak,
inlineStyle: el.style.cssText
});
return true;
}
return false;
}
Attack 4: max-height: min-content on text-only container — zero height when container has no block children
The min-content height of a container depends on its children. For a container with only anonymous inline text nodes (no explicit block children), the min-content height may resolve differently across browser implementations. In some contexts, particularly when the container has display: flow-root or unusual block formatting context establishment, max-height: min-content can resolve to the height of one text line — or even near-zero — when combined with specific overflow and text rendering properties:
/* Attack 4: max-height: min-content combined with line-clamp or single-line coercion */
/* The most reliable version uses line-clamp with min-content: */
.consent-disclosure {
max-height: min-content; /* clamp to intrinsic min height */
-webkit-line-clamp: 1; /* additional visual clamp to 1 line */
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
/* Combined: max-height:min-content sets a CSS max-height,
line-clamp independently clips to 1 line via overflow:hidden.
The first line of the disclosure is the visible region — everything else is clipped.
For a 20-line permission disclosure, 95% is hidden. */
}
/* Interaction with text-wrap: balance and min-content: */
.consent-disclosure {
width: min-content; /* narrow to longest word */
text-wrap: balance;
/* text-wrap:balance redistributes line lengths to minimize max line length.
In a min-content context, this may cause the browser to iteratively reduce
the content width further than min-content alone would produce. */
height: 1lh; /* exactly one line height */
overflow: hidden;
/* Only the first line is visible regardless of content length.
The expression '1lh' is the line-height unit — new in CSS Values Level 4.
Not a literal pixel value. Detection: check height relative to scrollHeight. */
}
/* max-height: min-content in a container with only positioned content: */
.consent-wrapper {
max-height: min-content;
overflow: hidden;
position: relative;
}
.consent-disclosure {
position: absolute; /* removes from flow */
/* Wrapper's min-content height = 0 (no in-flow children).
max-height: min-content = 0px.
overflow:hidden with a 0px max-height clips the absolutely positioned disclosure entirely.
The disclosure itself has full content, but the wrapper clipping makes it invisible. */
}
// Detection: check scrollHeight vs rendered height ratio
function detectMaxHeightMinContentClip(el) {
const wrapper = el.closest('[style*="max-height"]') || el.parentElement;
if (!wrapper) return false;
const cs = window.getComputedStyle(wrapper);
if (cs.overflow !== 'hidden' && cs.overflowY !== 'hidden') return false;
if (wrapper.scrollHeight > wrapper.clientHeight * 2 && el.textContent.trim().length > 40) {
console.error('SECURITY: consent wrapper clips >50% of disclosure content', {
wrapper,
wrapperClientHeight: wrapper.clientHeight,
wrapperScrollHeight: wrapper.scrollHeight,
disclosureTextLength: el.textContent.trim().length
});
return true;
}
return false;
}
Intrinsic sizing attacks bypass pixel-value thresholds: Many consent security guards check for height < 4px or width < 4px and flag those as attacks. Intrinsic sizing attacks produce dimensions derived from content metrics — a CJK min-content width might be 14px, passing the threshold. The guard then sees a 14px width element with 200 characters and no flag. A more robust check compares the element's physical dimensions against what would be needed to display its text content at the current font-size, line-height, and character count.
Attack summary
| Attack | CSS property | Affected text type | Effect | Severity |
|---|---|---|---|---|
| min-content CJK width collapse | width: min-content |
CJK / Arabic text | Width collapses to single character (~1em) | High |
| min-content height clipping | height: min-content; overflow: hidden |
Any multiline text | Height clips to ~1 line in narrow context | High |
| fit-content(0px) near-zero width | width: fit-content(0px); word-break: break-all |
Any text | Width = font-size (~14px), single char per line | High |
| max-height:min-content with abs children | max-height: min-content; overflow: hidden on parent |
Any text (in abs positioned child) | Parent height = 0, clips absolute child | High |
Consolidated finding blocks
width: min-content on a CJK-language disclosure collapses width to ~1em (one character), wrapping every character to a new line. In a height-constrained container with overflow: hidden, only 2-3 characters are visible. Detection: check getBoundingClientRect().width relative to text character count and font-size.
height: min-content combined with a narrow container and overflow: hidden clips a multiline disclosure to 1 line. Detection: el.scrollHeight > el.clientHeight × 1.5 on an element with overflow: hidden and text content > 50 characters.
width: fit-content(0px) combined with word-break: break-all and overflow: hidden collapses disclosure to single-character-wide column. Detection: computed width < 50px on element with text content > 30 characters.
max-height: min-content; overflow: hidden and no in-flow children has effective max-height of 0, clipping the absolutely-positioned consent disclosure entirely. Detection: wrapper scrollHeight > clientHeight × 2 with nested disclosure element having text content.