MCP server CSS block-size security: logical height collapse, writing-mode rotation bypass, zero collapse, and height gate bypass attacks
Published 2026-09-25 — SkillAudit Research
The CSS property block-size is the logical equivalent of height in the CSS Logical Properties specification. In a standard horizontal writing mode (writing-mode: horizontal-tb), block-size maps directly to the physical height of an element. In a vertical writing mode (writing-mode: vertical-rl or vertical-lr), the block and inline axes are rotated: block-size controls the physical width of the element, while inline-size controls the physical height.
This axis rotation is the fundamental source of block-size's attack surface in MCP server consent UIs. An adversary can set writing-mode: vertical-rl on a consent element and then set block-size: 0 — which collapses the width of the element to zero. Any audit script checking element.style.height or element.clientHeight will see normal values, because the vertical dimension is controlled by inline-size in this writing mode, not block-size. The audit passes while the consent element has zero width and is invisible.
Security gate bypass: Many MCP server consent verification scripts check element.style.height !== '0' or element.clientHeight > 0 before allowing acceptance. Setting block-size: 0 in a vertical writing mode collapses width, not height. clientHeight remains positive. element.style.height is empty. The security gate passes. The consent element has zero pixel width and is not visible.
Attack 1: block-size:0 with writing-mode:vertical-rl — width collapse evading height checks
In writing-mode: vertical-rl, the block axis runs left-to-right (the horizontal direction). Setting block-size: 0 sets the element's physical width to zero. The element is present in the DOM, has positive clientHeight (which is inline-size in this mode), but has zero width — it is effectively invisible as a vertical line.
/* Consent container collapsed via logical property in rotated writing mode */
.consent-section {
writing-mode: vertical-rl;
block-size: 0; /* = physical width: 0 in vertical-rl */
overflow: hidden; /* Clip overflowing inline content */
}
/* Result:
element.style.width: '' (not set)
element.style.height: '' (not set)
element.style.blockSize: '0'
element.clientWidth: 0 ← collapsed
element.clientHeight: N ← positive (inline axis = vertical in this mode)
Audit checks:
✓ element.style.height !== '0' → passes (height not set)
✓ element.clientHeight > 0 → passes (clientHeight is inline axis height)
✗ element.clientWidth > 0 → fails (0px wide = invisible)
Only an audit that checks getComputedStyle(el).blockSize or
that checks clientWidth independently detects the collapse. */
function detectBlockSizeCollapse(root) {
const findings = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
let el;
while (el = walker.nextNode()) {
const cs = window.getComputedStyle(el);
const blockSize = cs.getPropertyValue('block-size');
const parsed = parseFloat(blockSize);
if (!isNaN(parsed) && parsed < 4) {
const wm = cs.writingMode;
findings.push({
element: el,
blockSize: blockSize,
writingMode: wm,
clientWidth: el.clientWidth,
clientHeight: el.clientHeight,
note: `block-size:${blockSize} in writing-mode:${wm} — ${
(wm === 'vertical-rl' || wm === 'vertical-lr')
? 'physical width collapsed (height check will pass)'
: 'physical height collapsed (standard collapse)'
}`,
});
}
}
return findings;
}
Attack 2: block-size:1px — one-pixel consent strip
Setting block-size: 1px in a standard horizontal writing mode creates a one-pixel-tall consent element. The element has valid DOM content, a positive (but minimal) bounding rect, and is technically "visible". Audit scripts checking clientHeight > 0 pass. The one-pixel height means no text is legibly visible — a 16px font-size line overflows the 1px container and is clipped by overflow: hidden. The accept button, positioned after the consent element, may still be enabled because the consent section technically has positive height.
/* One-pixel consent strip */
.consent-terms {
block-size: 1px; /* = height: 1px in horizontal-tb */
overflow: hidden; /* Clip overflowing text */
/* Font-size: 14px. Each text line is 20px (line-height:1.4).
Visible: approximately 0 lines (1px < 20px).
The text overflows and is clipped by overflow:hidden.
Technically: the element has height=1px, not zero.
Audit: element.clientHeight = 1 → "visible" → gate passes.
User: sees a 1px horizontal line, no readable text.
Variant: block-size:0.5px (sub-pixel rendering)
Some browsers round 0.5px to 1px clientHeight.
Computed block-size resolves to 0.5px.
Visual: element may not render at all at sub-pixel sizes
on high-DPI displays where the physical pixel boundary matters. */
}
/* More subtle: block-size computed via CSS variable chain */
.subtle-collapse {
--consent-h: 0;
block-size: calc(var(--consent-h) * 1px);
/* If an injected style sets --consent-h to 0,
block-size resolves to 0px.
Audit checking element.style.blockSize sees 'calc(var(--consent-h) * 1px)' —
not '0'. Only resolution-time inspection reveals 0px. */
}
Attack 3: block-size overriding height-based security hardening
Some MCP server frameworks set min-height on consent containers to prevent collapse. CSS logical properties interact with physical properties: block-size takes precedence over height in the cascade when specificity is equal, because block-size is a more specific logical property that maps to height. But the interaction with min-height is asymmetric: min-block-size (logical) and min-height (physical) do not conflict — both apply, and the larger wins. The attack exploits a different path: setting both height and block-size on the same element from different cascade layers or specificity levels, where the physical min-height is set by the host and the logical block-size is injected.
/* Host framework hardening */
.consent-container {
min-height: 200px; /* Security gate: consent must be at least 200px tall */
}
/* MCP server injected style — same specificity, later in cascade */
.consent-container {
block-size: 0px; /* Logical block-size:0 */
/* In horizontal-tb writing mode:
block-size:0px conflicts with min-height:200px.
CSS resolution: min-height wins — it is a minimum constraint.
The computed height is max(0px, 200px) = 200px.
RESULT: this particular attack DOES NOT bypass min-height.
But consider: if the injected style ALSO sets writing-mode:vertical-rl,
then block-size:0px controls width (not height).
min-height still constrains the vertical dimension.
The element is 200px tall but 0px wide.
The security gate (min-height) passes. Consent is invisible. */
writing-mode: vertical-rl; /* Now block-size = physical width */
}
/* Detection: combined writing-mode rotation + block-size check */
function detectWritingModeBlockSizeBypass(root) {
const findings = [];
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
let el;
while (el = walker.nextNode()) {
const cs = window.getComputedStyle(el);
if (cs.writingMode === 'horizontal-tb') continue; // Only rotated modes
const blockSize = parseFloat(cs.getPropertyValue('block-size'));
const clientWidth = el.clientWidth;
if (!isNaN(blockSize) && (blockSize < 4 || clientWidth < 4)) {
findings.push({
element: el,
writingMode: cs.writingMode,
blockSize: cs.getPropertyValue('block-size'),
clientWidth, clientHeight: el.clientHeight,
minHeight: cs.minHeight,
note: 'Non-horizontal writing mode + near-zero block-size may bypass min-height gate by collapsing physical width instead',
});
}
}
return findings;
}
Attack 4: block-size in max-content / min-content keywords — consent shrink-wrapping
The block-size property accepts sizing keywords including min-content, max-content, and fit-content. Setting block-size: min-content shrinks the element's block dimension to the smallest it can be without clipping any content (in horizontal-tb, this means the element is exactly as tall as its content with no room for line wrapping beyond what the content requires). An adversary can pair this with a container that has an artificially small line length constraint — forcing the min-content height to be calculated against a narrow box, causing very long single-line text with no visible wrapping. In vertical writing modes, block-size: min-content shrinks the physical width to the minimum content width — often zero for elements with no inline content.
/* Shrink-wrap consent to minimum content size */
.consent-wrapper {
writing-mode: vertical-rl;
block-size: min-content; /* Physical width = min-content width */
/* If the consent text has no natural minimum width (all soft-wrapped),
min-content width in vertical-rl (which is the block axis) may be
very small — just the widest single word.
A 300-word consent block: min-content block-size ≈ widest word width.
This may be 80-120px. Combined with overflow:hidden, each line
of text overflows the 80-120px width and is clipped. */
}
/* With contain: size isolation */
.consent-contained {
contain: size;
block-size: min-content;
/* contain:size makes the browser treat the element as having no children
for sizing purposes. min-content resolves as if there is no content.
block-size resolves to 0px or close to it.
The contain:size isolation means the consent text does not influence
the parent layout, and the consent element itself collapses. */
}
Summary
| Attack | Mechanism | Severity | Detection method |
|---|---|---|---|
CRITICALblock-size:0 + writing-mode:vertical-rl |
Collapses physical width to 0 while clientHeight remains positive; height-based security gates pass | Consent element has 0px physical width; invisible; height audit passes | Check getComputedStyle(el).blockSize on all non-horizontal-tb writing mode elements |
HIGHblock-size:1px — one-pixel consent strip |
1px height clips all text via overflow:hidden; clientHeight=1 passes >0 check | No readable text visible; accept gate may pass on clientHeight > 0 check | Flag block-size or clientHeight < 4px on elements with consent text content |
HIGHblock-size via CSS variable chain |
block-size: calc(var(--x) * 1px) resolves to 0 when variable is 0; style string appears non-zero |
String-based audit sees non-zero; computed value is 0; element collapsed | Always resolve block-size at computed value, not at style string level |
MEDIUMblock-size:min-content + contain:size — zero collapse |
contain:size + min-content resolves to near-zero block-size; content not contributing to sizing | Consent element collapses; DOM content valid; contain isolation hides the attack | Check contain:size elements for near-zero block-size computed value |
See also: CSS height security for the physical property attacks, CSS inline-size security for the logical width equivalent, CSS writing-modes security for axis rotation attacks, and CSS logical properties overview.
SkillAudit checks all CSS logical properties — including block-size, inline-size, and their min/max variants — in both the static rule audit and the runtime computed-style check. Start a free scan.