Security reference · CSS logical properties · Max-height collapse · Consent hiding

MCP server CSS max-block-size security

CSS max-block-size is the CSS Logical Properties equivalent of max-height in horizontal writing modes. Setting max-block-size: 0 with overflow: hidden collapses a consent element to zero visible height — producing the same visual result as max-height: 0; overflow: hidden without the string max-height appearing anywhere in the stylesheet. Physical-property scanners and auditors who check getComputedStyle().maxHeight directly will see the resolved value (correctly 0px), but string-based CSS source scanners checking for max-height miss it entirely. Four attack patterns: direct collapse, 1px hairline variant, custom property obfuscation, and JS class-toggle at mousedown with CSS height transition.

CSS max-block-size property fundamentals

In the CSS Logical Properties specification, the physical dimensions width and height are generalized to inline-size and block-size, which map to the physical axes relative to the current writing mode. In horizontal writing modes (writing-mode: horizontal-tb, the default for all Latin-script content), block-size corresponds to height and inline-size corresponds to width. Consequently, max-block-size maps to max-height and min-block-size maps to min-height. The browser resolves these to physical property values in computed style — getComputedStyle(el).maxHeight correctly returns the value set via max-block-size.

Logical propertyPhysical equivalent (horizontal)Computed style accessorString "max-height" in source?
max-block-size: 0max-height: 0getComputedStyle().maxHeight → "0px"No
max-block-size: 1pxmax-height: 1pxgetComputedStyle().maxHeight → "1px"No
max-block-size: var(--h)max-height: (resolved)getComputedStyle().maxHeight → resolved valueNo
max-height: 0max-height: 0getComputedStyle().maxHeight → "0px"Yes

Attack surface: SA-CSS-MBSZ-001 — direct max-block-size:0 collapse

max-block-size: 0; overflow: hidden collapses the consent element to zero rendered height. The element still participates in layout (not removed like display:none), occupies its natural horizontal space, and may have visible left/right borders or a top border hairline if border-width exceeds 0. Text content is in the DOM and read by accessibility APIs. The computed value of maxHeight is 0px — detectable via computed style — but the property name max-height never appears in any stylesheet.

/* MCP inject — collapses consent without max-height keyword */
.mcp-consent-section {
  max-block-size: 0;
  overflow: hidden;
  /* "max-height" not in stylesheet — keyword scanner misses this */
}

Detection: parseFloat(getComputedStyle(consentEl).maxHeight) === 0 combined with el.scrollHeight > 0 (content exists but is fully collapsed). Always read from computed style, not from source CSS.

Attack surface: SA-CSS-MBSZ-002 — 1px hairline variant

max-block-size: 1px; overflow: hidden clips consent text to a 1px visible band at the top of the element. At 14–16px line-height, a 1px clip shows only the very top pixel of the first line's ascenders — a thin stroke that resembles a decorative divider, not legible text. This variant passes any check of the form maxHeight === '0px' since the value is 1px, not 0px. At most viewing distances and screen densities, the 1px visible hairline is not interpretable as consent text content.

/* SA-CSS-MBSZ-002: 1px hairline — passes "is it zero" check */
.mcp-disclosure-panel {
  max-block-size: 1px;
  overflow: hidden;
  /* getComputedStyle().maxHeight: "1px" — not "0px" — passes zero-check */
}

Detection threshold: parseFloat(getComputedStyle(consentEl).maxHeight) < 12 with el.scrollHeight > 20 — any max-height below one line-height that still has content behind it indicates this pattern.

Attack surface: SA-CSS-MBSZ-003 — CSS custom property obfuscation

The logical property attack is combined with custom property indirection: --mcp-dialog-height: 0; max-block-size: var(--mcp-dialog-height). The computed value of maxHeight is still 0px — the browser resolves both layers — but a static CSS source scanner looking for max-block-size: 0 finds only max-block-size: var(--mcp-dialog-height). The custom property definition may be in a separate stylesheet, an inline :root rule, or set via JS: document.documentElement.style.setProperty('--mcp-dialog-height', '0').

/* SA-CSS-MBSZ-003: custom property + logical property double indirection */
:root { --mcp-panel-block: 0; }
.consent-container {
  max-block-size: var(--mcp-panel-block);
  overflow: hidden;
}
/* Two layers of indirection: logical property + var() reference */
/* Both resolved by getComputedStyle().maxHeight → "0px" */

Key: Always use getComputedStyle(el).maxHeight — it resolves var() and logical property mappings simultaneously. No scanner that reads CSS source text can reliably detect this double-indirection pattern.

Attack surface: SA-CSS-MBSZ-004 — JS class-toggle collapse at mousedown with CSS transition

The consent element starts at full height (max-block-size: 200px or none), and MCP JS adds a class at mousedown that sets max-block-size: 0 with a CSS transition: transition: max-block-size 0.2s ease-in. The consent text smoothly collapses over 200ms at the moment the user initiates the install action. The transition makes the collapse look like an intentional accordion animation rather than a consent-hiding attack. At load time (before mousedown), computed maxHeight is 200px — all load-time audits pass.

/* SA-CSS-MBSZ-004: class-toggle collapse at mousedown */
.consent-panel {
  max-block-size: 200px;
  overflow: hidden;
  transition: max-block-size 0.2s ease-in;
}
.consent-panel.collapsing {
  max-block-size: 0; /* applied by JS at mousedown */
}

// MCP JS:
installBtn.addEventListener('mousedown', () => {
  consentEl.classList.add('collapsing');
  // smooth collapse before click fires
});

Detection: MutationObserver on class attribute of consent elements. After detecting class change, re-check getComputedStyle(consentEl).maxHeight. If it has changed from a non-zero value to 0px (or near zero), flag SA-CSS-MBSZ-004. Also simulate mousedown on the install button and re-check consent dimensions after the interaction.

Findings summary

Critical
SA-CSS-MBSZ-001 — max-block-size:0 logical property collapses consent to zero height; "max-height" not in stylesheet; physical-property source scanners miss it. getComputedStyle().maxHeight → "0px".
High
SA-CSS-MBSZ-002 — max-block-size:1px hairline; passes "is max-height zero" check; clips all consent text to sub-legibility 1px band. Threshold check at <12px required.
Critical
SA-CSS-MBSZ-003 — Custom property + logical property double indirection; CSS source scanner sees only var() reference; computed maxHeight resolves to 0px. Static scanning cannot catch this.
Critical
SA-CSS-MBSZ-004 — JS class-toggle at mousedown applies max-block-size:0 with CSS transition; smooth 200ms collapse at install time; passes all load-time audits. MutationObserver + interaction simulation required.

Key insight

Read getComputedStyle(el).maxHeight — this resolves both the logical property mapping and any var() chains. Apply a threshold check: parseFloat(maxHeight) < 12 with el.scrollHeight > 20 flags all zero and near-zero max-height collapse variants regardless of which CSS property was used to set them. For deferred collapse, add a MutationObserver on consent element class and style attributes.

See also: max-height physical property attacks, inline-size logical width attacks, CSS logical property attacks on MCP consent UIs.