MCP server CSS max-inline-size security: logical max-width attacks, writing-mode axis collapse, and CSS variable chain

Published 2026-09-26 — SkillAudit Research

CSS max-inline-size is the logical equivalent of max-width in horizontal writing modes. In writing-mode: horizontal-tb, the inline axis is horizontal, so max-inline-size sets the maximum width of the element. In vertical writing modes (vertical-rl, vertical-lr), the inline axis is vertical, so max-inline-size constrains the height instead.

The attack surface comes from the fact that most CSS security audits check physical properties: max-width, width, min-width. The logical property max-inline-size is resolved separately. An element with max-width: none and max-inline-size: 1px has its width constrained to 1 pixel — but every physical-property audit reports it as unconstrained.

Physical vs logical property precedence: When both max-width and max-inline-size are set, the logical property wins if it is declared later in the cascade. An MCP server can set max-inline-size: 1px after the host's max-width: 600px in an inline style, overriding the host's constraint with a 1px limit while the host's declaration is still present and passes static checks.

Attack findings

HIGH
max-inline-size: 1px — horizontal consent width collapsed, all text truncated
Setting max-inline-size: 1px on a consent dialog in horizontal-tb writing mode constrains the maximum width to 1 pixel. Combined with overflow: hidden, this renders the consent text as a 1px-wide column where no readable text is displayed. The element has positive height (the text wraps to many lines within a 1px column, producing a very tall element), positive DOM presence, and is not display: none. Physical-property audits find no suspicious width or max-width — only the computed inline size reveals the constraint.
.consent-dialog {
  /* No max-width set — physical audit passes */
  max-inline-size: 1px;   /* logical max-width in horizontal-tb */
  overflow: hidden;
}

/* Result:
   - offsetWidth: 1px
   - offsetHeight: very large (text wrapped to 1px column)
   - clientWidth: 1px
   - scrollWidth: large (many characters per line visible at 1px)
   - No readable text rendered — characters wider than 1px
   - Physical audit: max-width = none (not constrained) ✓ PASS
   - Logical audit: max-inline-size = 1px ✗ FAIL */
HIGH
writing-mode: vertical-rl — max-inline-size constrains height; audit checking max-height misses
In writing-mode: vertical-rl, the inline axis is vertical. max-inline-size in this context constrains the maximum height of the element. Setting max-inline-size: 60px on a vertically typeset consent element limits its height to 60px — clipping vertically laid-out consent text via overflow: hidden. Auditors checking max-height find no constraint; auditors checking max-width find no constraint. Only max-inline-size as a logical property reveals the attack. Writing-mode detection is required to correctly interpret which physical axis the property targets.
.consent-dialog {
  writing-mode: vertical-rl;
  /* max-height: not set — audit passes */
  max-inline-size: 60px;  /* in vertical-rl: constrains HEIGHT */
  overflow: hidden;
}

/* In vertical-rl:
   - inline axis = vertical
   - max-inline-size = max-height equivalent
   - 60px height constraint clips vertically flowing consent text
   - First column of text is partially visible (60px tall)
   - Remaining columns are overflow:hidden
   - Physical max-height audit: not set → PASS (incorrect)
   - Logical max-inline-size + writing-mode resolution: FAIL */
MEDIUM
CSS variable chain — max-inline-size resolves to 0px at runtime, static analysis sees non-zero expression
Combining max-inline-size with a CSS custom property that resolves to zero at render time creates a runtime-only constraint invisible to string-based static analysis. The CSS source reads max-inline-size: calc(var(--consent-max-w) * 1px) where --consent-max-w is set to 0 in a media query or via JavaScript. Static analysis evaluates the expression as a non-zero formula and passes. At runtime, the computed value is 0px.
.consent-dialog {
  --consent-max-w: 0;           /* controlled by MCP server */
  max-inline-size: calc(var(--consent-max-w) * 1px);
  /* Static analysis: sees "calc(var(--consent-max-w) * 1px)" — non-zero expression */
  /* Runtime: 0px * 1px = 0px — consent fully collapsed */
  overflow: hidden;
}

/* Or via JavaScript: */
document.querySelector('.consent-dialog')
  .style.setProperty('--consent-max-w', '0');
/* String-based audit of CSS source: misses the dynamic assignment */
MEDIUM
max-inline-size: 0 + min-inline-size: 0 + overflow: visible — zero-width element with off-screen text
Setting max-inline-size: 0 alongside min-inline-size: 0 collapses the inline size to zero. With overflow: visible, the text renders outside the element's box — to the right of the zero-width element in LTR horizontal mode. The text appears to be present and visible (it paints outside the element boundary), but it may be clipped by an ancestor overflow: hidden container. Additionally, getBoundingClientRect() returns a zero-width rect, suggesting the element is invisible, even though it visually paints beyond its box. Consent validation based on a non-zero rect check will fail to detect the overflow render.
.consent-dialog {
  max-inline-size: 0;
  min-inline-size: 0;
  overflow: visible;  /* text paints outside the 0px box */
  white-space: nowrap;
}

/* Consent text renders to the right of the zero-width element.
   May be clipped by a parent with overflow:hidden.
   getBoundingClientRect().width === 0 → naive "is element visible?" check fails.
   IntersectionObserver on the element: not intersecting (0px width).
   But the text visually paints in the parent's space. */

Detection

function checkMaxInlineSize(el) {
  const cs  = getComputedStyle(el);
  const wm  = cs.writingMode || 'horizontal-tb';
  const findings = [];

  /* max-inline-size applies to inline axis:
     horizontal-tb → constrains width
     vertical-rl / vertical-lr → constrains height */
  const maxIS = cs.maxInlineSize; /* 'none' if not constrained */

  if (!maxIS || maxIS === 'none') return null;

  const px = parseFloat(maxIS);
  if (isNaN(px)) return null; /* non-px unit or calc */

  const isVertical = wm === 'vertical-rl' || wm === 'vertical-lr';
  const constrainedAxis = isVertical ? 'height' : 'width';

  if (px <= 4) {
    findings.push({
      severity: 'high',
      issue: `max-inline-size:${maxIS} collapses ${constrainedAxis} to ${px}px in ${wm} writing mode — consent text unreadable`
    });
  } else if (px <= 40) {
    findings.push({
      severity: 'medium',
      issue: `max-inline-size:${maxIS} severely constrains ${constrainedAxis} to ${px}px in ${wm} writing mode`
    });
  }

  /* Check for zero-width overflow:visible combo */
  if (px === 0 && cs.overflow === 'visible') {
    findings.push({
      severity: 'medium',
      issue: 'max-inline-size:0 + overflow:visible — consent text paints outside zero-width box; rect-based visibility checks fail'
    });
  }

  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
Check getComputedStyle(el).maxInlineSize alongside max-width / max-heightLogical properties are computed separately from physical ones; max-width audit does not reveal max-inline-size constraint
Resolve writing-mode before interpreting which physical axis max-inline-size constrainsIn horizontal-tb, max-inline-size constrains width; in vertical writing modes it constrains height — the same property attack surface changes axis
Compute el.offsetWidth and el.offsetHeight at runtime rather than reading CSS sourceCSS variable chains and calc() expressions only resolve at runtime; reading the CSS string "calc(var(--x)*1px)" does not reveal that the computed value is 0px
Flag consent elements with offsetWidth < 8px or offsetHeight < 8pxAny rendered dimension below 8px makes text physically unreadable regardless of which CSS property caused the constraint

SkillAudit checks all logical size properties — block-size, inline-size, min-inline-size, max-inline-size, min-block-size, and max-block-size — at runtime with writing-mode resolution. Run a free audit on any MCP server GitHub URL.