Security Guide

MCP server CSS min-block-size security — min-block-size:100vh pushing consent below fold, min-inline-size:0 column collapse, writing-mode axis swap, JS mousedown injection

CSS min-block-size and min-inline-size are logical min-sizing properties that set minimum sizes along the block axis and inline axis respectively. In the default writing-mode: horizontal-tb, min-block-size = min-height and min-inline-size = min-width. For MCP servers with CSS injection capability, these properties create attack surfaces by forcing consent dialog containers to unexpected sizes: min-block-size: 100vh expands the dialog to fill the entire viewport, pushing the consent disclosure text above the approve button far below the initially-visible area, and min-inline-size combined with max-inline-size creates a narrow unavoidable column for consent text.

CSS min-block-size / min-inline-size — property overview

The min-block-size property sets the minimum size of an element along the block axis — the axis perpendicular to the inline content flow direction. In horizontal text (writing-mode: horizontal-tb), the block axis is vertical, so min-block-size is equivalent to min-height. In vertical text (writing-mode: vertical-rl or vertical-lr), the block axis is horizontal, so min-block-size is equivalent to min-width. Similarly, min-inline-size maps to min-width in horizontal text and min-height in vertical text. The security implication: physical-property defenses (min-height, min-width) will not catch logical-property attacks when the writing mode differs from the assumed default, creating the same logical-vs-physical detection gap seen with border-block and border-inline.

Attack 1: min-block-size: 100vh — forcing consent dialog to full viewport height

A consent dialog container with min-block-size: 100vh is forced to a minimum height equal to the full viewport height. If the consent dialog is normally a modal that is 300–400px tall — showing the disclosure text, the permission list, and the approve/deny buttons all in the visible window — expanding it to 100vh (typically 900px on a laptop) separates the top content (disclosure text) from the bottom content (approve button) by hundreds of pixels. The user sees the top portion of the dialog with the disclosure text, scrolls past blank space to reach the approve button, and may miss the connection between the two. More severely: if the dialog doesn't have a scrollbar (overflow: hidden), the approve button is pushed below the viewport entirely and is accessible only via keyboard Tab navigation.

/* Attack 1: min-block-size:100vh forces dialog to full viewport height */

.consent-dialog-container,
.mcp-permission-modal,
.auth-dialog {
  min-block-size: 100vh !important;  /* = min-height: 100vh in horizontal-tb */
  /* Do NOT set overflow:hidden — let the dialog be scrollable
     so the button is technically reachable (to avoid detection via BCR checks)
     but far below the initially-visible area */
}

/* Effect for a 400px-tall consent dialog on a 900px viewport:
   Before: dialog = 400px, all content visible in one screen
   After:  dialog = 900px (min-block-size), content distributed over 900px

   Layout:
   [0–80px]:    Dialog header ("MCP Permission Request")   ← visible
   [80–160px]:  Security disclosure text                   ← visible
   [160–300px]: Permission list                            ← visible
   [300–500px]: BLANK SPACE (expanded by min-block-size)
   [500–560px]: Approve / Deny buttons                     ← BELOW FOLD

   User behavior: reads disclosure, sees no button, may assume a loading
   state and wait. The approve button is 500px below the visible area.
   With overflow:scroll: user must scroll 200px past blank space to find buttons.

   getComputedStyle detection:
   - minBlockSize: '100vh' or '900px' (resolved) → DETECTABLE
   - getBoundingClientRect().height: 900px → significantly taller than normal
   - Buttons: getBoundingClientRect().top > window.innerHeight → out of viewport */

/* Conservative variant (less obvious): */
.consent-dialog-container {
  min-block-size: 80vh !important;  /* pushes button below fold on most viewports */
}

The approve button is DOM-present and accessible, but not visible. All DOM checks on the approve button pass: it is not hidden, not display:none, not visibility:hidden, and not zero-sized. Its getBoundingClientRect() returns a non-zero rect — but with top > window.innerHeight, indicating it is below the viewport. Most consent auditors check that the button exists and is enabled; few check that the button's bounding rect places it within the visible viewport before the user scrolls.

Attack 2: min-inline-size: 0 + max-inline-size — forced narrow column for consent text

The min-inline-size: 0 property on a consent text element removes the natural minimum size floor, allowing max-inline-size to constrain the element to a very narrow column without the browser auto-expanding to fit content. Combined with overflow: hidden, this forces consent text into a narrow visible window. Normally, a container without min-inline-size: 0 will expand to at least fit the longest word in its content — preventing full text occlusion. Explicitly setting min-inline-size: 0 removes this floor, enabling the max-inline-size constraint to win.

/* Attack 2: forced narrow column via min-inline-size:0 + max-inline-size */

.consent-text, .permission-disclosure {
  min-inline-size: 0 !important;   /* remove natural minimum-width floor */
  max-inline-size: 80px !important; /* constrain to narrow column (3-4 chars wide) */
  overflow: hidden !important;      /* clip content outside narrow column */
  /* Alternatively: max-inline-size:15ch — 15 characters wide
     "Allow SkillAudit to access your filesystem" wraps to:
     Line 1: "Allow"
     Line 2: "SkillAudit"
     Line 3: "to access"
     Line 4: "your filesys"   (truncated by overflow:hidden at fixed height)
     Line 5: "tem"            ← CLIPPED if max-block-size also set */
}

/* With fixed height + overflow:hidden, only first N lines visible:
   max-block-size: 2em → shows only "Allow / SkillAudit"
   The permission verb ("to access"), the object ("filesystem"), and the scope
   are all clipped outside the visible area.
   textContent: full string unchanged ← detection passes
   scrollHeight > clientHeight: detectable ← detection catches */

Attack 3: writing-mode axis swap — logical min-size moves to unexpected physical axis

In writing-mode: vertical-rl, the block axis is horizontal and the inline axis is vertical. An MCP server that can inject writing-mode: vertical-rl on the consent container and then set min-block-size: 100vw forces the container to full viewport width along the horizontal (block) axis. A security defense checking getComputedStyle(el).minHeight will see 0px or the natural height value — because in vertical writing mode, min-block-size resolves to the physical width constraint, not height. Only checking getComputedStyle(el).minBlockSize (the logical property) or both physical axes detects the attack.

/* Attack 3: writing-mode swap makes min-block-size a width constraint */

.consent-dialog {
  writing-mode: vertical-rl !important;      /* block axis = horizontal */
  min-block-size: 100vw !important;          /* in vertical-rl: = min-width: 100vw */
  /* Consent text is now rotated 90° AND the dialog is forced to full viewport width */
}

/* getComputedStyle(el).minHeight: '0px' ← physical check MISSES the constraint
   getComputedStyle(el).minBlockSize: '100vw' / '1200px' ← logical check catches it
   getComputedStyle(el).writingMode: 'vertical-rl' ← rotation detectable separately */

Attack 4: JS mousedown injection of min-block-size

/* Mousedown-only min-block-size injection */
(function() {
  document.querySelectorAll('.approve-btn, [data-action="allow"]').forEach(btn => {
    btn.addEventListener('mousedown', () => {
      const dialog = document.querySelector('.consent-dialog,.permission-modal');
      if (dialog) {
        dialog.style.minBlockSize = '100vh';
        /* This instantly pushes button below fold during the mousedown event,
           before the click event fires. The button is still clicked (mousedown
           and click can fire on different positions in some implementations)
           but the user sees the disclosure pushed far from the button they clicked. */
      }
    }, { passive: true });
    btn.addEventListener('mouseup', () => {
      const dialog = document.querySelector('.consent-dialog,.permission-modal');
      if (dialog) dialog.style.minBlockSize = '';
    }, { passive: true });
  });
})();

Detection summary

HIGH min-block-size50vh (or resolved value ≥ 50% of viewport height) on a consent dialog container — dialog forced to large height, approve button likely below fold.
HIGH Approve button getBoundingClientRect().top > window.innerHeight — button is below the visible viewport without user scrolling.
MEDIUM max-inline-size15ch or ≤ 120px on a consent text element — text forced into an extremely narrow column; combined with overflow: hidden, portions of the permission string clip.
MEDIUM writing-mode is not horizontal-tb on consent container — logical axis swap; check min-block-size and min-inline-size against both physical axes.
/* Detection: check min-block-size on consent containers */
function checkMinBlockSize(dialogEl) {
  const cs = getComputedStyle(dialogEl);
  const mbs = cs.getPropertyValue('min-block-size');
  const mis = cs.getPropertyValue('min-inline-size');
  const wm  = cs.writingMode;
  const resolvedMinH = parseFloat(mbs) || 0;
  const vh = window.innerHeight;

  return {
    minBlockSizeAttack: resolvedMinH >= vh * 0.5,
    writingModeRotated: wm !== 'horizontal-tb',
    approveButtonBelowFold: (() => {
      const btn = dialogEl.querySelector('.approve-btn, [data-action="allow"]');
      return btn ? btn.getBoundingClientRect().top > vh : false;
    })(),
  };
}

SkillAudit checks min-block-size, min-inline-size, and button viewport visibility during MCP server audits — including verifying that the approve and deny buttons are within the initially-visible viewport area before any user scrolling. Defenses that check only physical min-height / min-width properties miss logical-property attacks in non-default writing modes. Run a free audit on your MCP server.