MCP server CSS overflow-block and overflow-inline security: logical overflow axis swap, writing-mode hidden clipping, directional scroll trap, and combined logical overflow attacks

Published 2026-07-24 — SkillAudit Research

CSS Logical Properties (CSS Logical Properties and Values Level 1) introduced flow-relative equivalents for physical CSS properties. Instead of overflow-x (horizontal clipping) and overflow-y (vertical clipping), authors can write overflow-inline and overflow-block — which map to physical axes based on the element's writing mode.

In the default horizontal left-to-right writing mode (writing-mode: horizontal-tb), the mapping is straightforward: overflow-block = overflow-y (block axis = vertical), overflow-inline = overflow-x (inline axis = horizontal). But under writing-mode: vertical-lr or writing-mode: vertical-rl, the axes swap: overflow-block becomes the horizontal axis and overflow-inline becomes the vertical axis.

MCP server consent-disclosure attacks exploit this mapping ambiguity. Scanners that look for overflow-y: hidden or overflow-x: hidden do not match overflow-block: hidden or overflow-inline: hidden — even though the computed effect is identical. This article documents four attacks using these logical properties.

Browser support: overflow-block and overflow-inline are supported in Chrome 87+, Firefox 69+, and Safari 15+. On older browsers, these declarations are ignored (the overflow falls back to the browser default, usually visible — which is also an attack path if the fallback is the only visible form of the disclosure).

Attack 1: overflow-block: hidden — vertical clipping bypass in horizontal writing mode

In the default writing-mode: horizontal-tb, overflow-block: hidden is exactly equivalent to overflow-y: hidden. An MCP server can use the logical property to clip tall consent disclosures while bypassing scanners that only check for overflow-y: hidden:

/* Attack 1: overflow-block: hidden on a height-constrained consent disclosure */

/* Direct (detected by scanners checking overflow-y): */
.consent-container {
  height: 20px;
  overflow-y: hidden;   /* scanner matches this */
}

/* Logical property bypass: */
.consent-container {
  height: 20px;
  overflow-block: hidden;  /* same computed effect — clips vertical overflow */
  /* overflow-block in horizontal-tb = overflow-y.
     A scanner matching /overflow-y\s*:\s*hidden/ does NOT match this.
     A scanner matching /overflow:\s*hidden/ does NOT match this
     (shorthand 'overflow' expands to overflow-x and overflow-y, not the logical
     properties — the two property namespaces are independent in the CSSOM). */
}

/* Combined with a height that seems to allow content: */
.consent-container {
  /* Appears to allow one line of 16px text: */
  height: 18px;
  line-height: 16px;
  overflow-block: hidden;
  /* The disclosure text contains 4 paragraphs. Only the first ~18px is visible.
     The rest of the content is clipped by overflow-block: hidden.
     getComputedStyle(el).overflowY returns 'hidden' (the browser resolves the
     logical property to its physical equivalent in computed style).
     BUT: CSS scanners reading the stylesheet source text do not see 'overflow-y'. */
}

// Detection: check computed overflow values (physical axes)
function detectLogicalOverflowClipping(el) {
  const cs = window.getComputedStyle(el);
  // getComputedStyle always returns the physical properties — logical properties
  // are always resolved to their physical equivalents in computed style.
  const overflowY = cs.overflowY;
  const overflowX = cs.overflowX;

  // Check for height-constrained hidden overflow clipping
  const height = parseFloat(cs.height);
  const scrollHeight = el.scrollHeight;

  if ((overflowY === 'hidden' || overflowY === 'clip') && scrollHeight > height * 1.5) {
    console.error('SECURITY: consent disclosure is clipped by overflow-block/overflow-y: hidden', {
      element: el,
      computedOverflowY: overflowY,
      visibleHeight: height,
      totalScrollHeight: scrollHeight,
      hiddenFraction: ((scrollHeight - height) / scrollHeight).toFixed(2)
    });
    return true;
  }
  return false;
}

Attack 2: writing-mode: vertical-lr + overflow-block: hidden — axis swap clipping

Under a vertical writing mode, the block axis is horizontal. This means overflow-block: hidden clips horizontal overflow while appearing to be a vertical-axis property to a scanner that reads the name "block" as "vertical." MCP servers exploit this to clip wide consent disclosures in horizontally-scrollable contexts:

/* Attack 2: writing-mode swap — overflow-block clips horizontal axis */

/* Under writing-mode: vertical-lr:
   - block axis = horizontal (left-to-right direction of the block flow)
   - inline axis = vertical (direction of text lines within a block)
   Logical property mapping flips:
   - overflow-block (block axis) → clips HORIZONTAL overflow
   - overflow-inline (inline axis) → clips VERTICAL overflow */

.consent-wrapper {
  writing-mode: vertical-lr;  /* established on the ancestor */
}

.consent-disclosure {
  /* Under vertical-lr writing mode: */
  overflow-block: hidden;   /* clips HORIZONTAL (block direction in vertical-lr) */
  overflow-inline: scroll;  /* allows VERTICAL scroll (inline direction) */

  /* The consent disclosure text, which is in English horizontal text,
     appears as a single rotated column in vertical writing mode.
     The user can scroll vertically (overflow-inline: scroll) but cannot
     see content that extends horizontally (overflow-block: hidden).
     English text is designed to be read left-to-right in horizontal lines —
     in vertical-lr mode, the text is rotated 90°, making it extremely difficult
     to read even when visible. The disclosure is technically present but
     practically illegible. */
}

/* Even subtler: reset writing mode on the disclosure but inherit overflow */
.consent-wrapper {
  writing-mode: vertical-lr;
  overflow-block: hidden;  /* clips horizontal = the layout's natural block direction */
}

.consent-disclosure {
  writing-mode: horizontal-tb;  /* text reads normally left-to-right */
  /* But overflow-block: hidden inherited from parent clips the block axis
     of the parent (horizontal), constraining the horizontal space of this disclosure. */
}

/* Detection must account for the writing-mode context: */
function detectAxisSwapOverflowClipping(el) {
  const cs = window.getComputedStyle(el);
  const writingMode = cs.writingMode;
  const overflowY = cs.overflowY;
  const overflowX = cs.overflowX;

  if ((writingMode === 'vertical-lr' || writingMode === 'vertical-rl') &&
      (overflowX === 'hidden' || overflowX === 'clip')) {
    // In vertical writing mode, overflow-block corresponds to overflow-x
    if (el.scrollWidth > parseFloat(cs.width) * 1.2) {
      console.error('SECURITY: vertical writing-mode with overflow-block: hidden clips disclosure width', {
        element: el,
        writingMode,
        computedOverflowX: overflowX,
        scrollWidth: el.scrollWidth,
        clientWidth: parseFloat(cs.width)
      });
      return true;
    }
  }
  return false;
}

Attack 3: overflow-inline: hidden on a width-constrained container — horizontal scroll trap

In horizontal writing mode, overflow-inline: hidden clips horizontal overflow, identical to overflow-x: hidden. When combined with a narrow container width, it creates a single-line viewport that clips a multi-line disclosure to one line of text:

/* Attack 3: overflow-inline: hidden + narrow container = single-line viewport */

/* In horizontal-tb writing mode:
   overflow-inline = overflow-x (inline axis = horizontal direction of text) */

.consent-container {
  /* Constrain to single-line width: */
  width: 40px;                 /* very narrow — only a few characters visible */
  white-space: nowrap;         /* prevent text from wrapping to next line */
  overflow-inline: hidden;     /* clips horizontal overflow = overflow-x: hidden */
  /* Effect: the disclosure text ('By clicking OK you authorize...') renders on a
     single line 40px wide, then clips. Only the first ~6 characters are visible.
     The user sees 'By cli...' — not the full disclosure.

     Scanner looking for overflow-x: hidden → no match.
     Scanner looking for width: 40px → matches but width alone isn't flagged. */
}

/* Variant: make the constraint responsive so it's harder to detect at fixed viewport: */
.consent-container {
  max-width: 10%;  /* 10% of parent = very narrow in most UI contexts */
  overflow-inline: hidden;
  white-space: nowrap;
  /* At 400px container: max-width = 40px — same attack as above.
     At 1200px container: max-width = 120px — wider but still clips.
     The % value bypasses scanners checking for literal small px values. */
}

/* Combined with text-overflow: ellipsis to make truncation look "normal": */
.consent-container {
  width: 60px;
  overflow-inline: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  /* The user sees 'By cl...' — the ellipsis makes it look like a UI truncation
     pattern, not an attack. This is a social-engineering amplifier. */
}

// Detection: check for narrow containers with overflow-inline (computed overflow-x) hidden
function detectInlineOverflowNarrowContainer(el) {
  const cs = window.getComputedStyle(el);
  const overflowX = cs.overflowX;  // always resolves logical to physical
  const width = parseFloat(cs.width);

  if ((overflowX === 'hidden' || overflowX === 'clip') &&
      width < 100 && el.textContent.trim().length > 20) {
    console.error('SECURITY: consent disclosure clipped by narrow container + overflow-inline: hidden', {
      element: el,
      computedOverflowX: overflowX,
      containerWidth: width,
      textContentLength: el.textContent.trim().length,
      // First 30 chars are visible; rest is clipped
      visibleText: el.textContent.trim().substring(0, Math.floor(width / 8))
    });
    return true;
  }
  return false;
}

Attack 4: overflow-block: scroll + overflow-inline: hidden — directional scroll trap

When an MCP server sets overflow-block: scroll (vertical scroll) and overflow-inline: hidden (horizontal clip) on a height-constrained container, it creates a deceptive scrollable region. The user can scroll vertically, but the container is short enough that the disclosure text appears to fit without scrolling — in reality the remaining content is below the visible fold in the short container:

/* Attack 4: overflow-block: scroll + overflow-inline: hidden = scroll trap */

/* In horizontal writing mode:
   overflow-block = overflow-y (block axis = vertical)
   overflow-inline = overflow-x (inline axis = horizontal) */

.consent-container {
  height: 40px;              /* tall enough for ~2 lines — seems reasonable */
  overflow-block: scroll;    /* vertical scrollbar appears if overflow exists */
  overflow-inline: hidden;   /* clips horizontal overflow */

  /* The disclosure text is 8 paragraphs = ~200px of content at 14px/1.5 line-height.
     The container shows 2 lines. The vertical scrollbar appears, suggesting the user
     should scroll — but in a rapid UI flow (popup, install dialog), users rarely
     scroll consent text in small boxes. 160px of disclosure is hidden below.

     Scanner matching 'overflow-y: scroll' + 'height: 40px' → no match.
     This combined logical property form bypasses both checks. */
}

/* Paired with max-height to look like a normal scrollable box: */
.consent-container {
  max-height: 3em;           /* 3 lines — seems reasonable for a "summary" */
  overflow-block: scroll;    /* "scroll if needed" appears innocent */
  overflow-inline: hidden;   /* technical bypass */
  font-size: 13px;
  line-height: 1.5;          /* 3em × 13px × 1.5 = 58.5px visible height */
  /* Actual content: 500 words of permissions.
     User must scroll 8× the visible height to read all content.
     Most users do not — the first 3 lines are visible bait-and-switch. */
}

// Detection: scrollable-but-clipped consent container
function detectScrollTrapContainer(el) {
  const cs = window.getComputedStyle(el);
  const overflowY = cs.overflowY;
  const height = parseFloat(cs.height);
  const scrollHeight = el.scrollHeight;

  // A scrollable disclosure container that requires scrolling more than 2× its height
  // to see all content is a scroll trap — consent cannot be obtained this way.
  if ((overflowY === 'scroll' || overflowY === 'auto') &&
      scrollHeight > height * 2 &&
      el.textContent.trim().length > 50) {
    console.error('SECURITY: consent disclosure in scroll trap — user must scroll to see full content', {
      element: el,
      computedOverflowY: overflowY,
      visibleHeight: height,
      totalScrollHeight: scrollHeight,
      scrollRatio: (scrollHeight / height).toFixed(1),
      // Check if user has actually scrolled
      currentScrollTop: el.scrollTop,
      percentSeen: ((el.scrollTop + height) / scrollHeight * 100).toFixed(0) + '%'
    });
    return true;
  }
  return false;
}

Why logical overflow properties evade CSS scanners: CSS security scanners that check for disclosure-hiding patterns typically build pattern lists from physical property names: overflow-x: hidden, overflow-y: hidden, overflow: hidden. The logical properties overflow-block and overflow-inline produce identical computed effects but do not match these patterns. Critically, the physical overflow properties in getComputedStyle() always resolve to their computed values — so any runtime detection check using getComputedStyle will correctly identify the physical overflow and catch the attack. Only static CSS source text scanners are blind to the logical-to-physical mapping.

Attack summary

Attack CSS property used Physical equivalent Effect Severity
Vertical clip via overflow-block overflow-block: hidden overflow-y: hidden Clips multi-paragraph disclosure to one line of visible height High
Axis swap via writing-mode + overflow-block writing-mode: vertical-lr; overflow-block: hidden overflow-x: hidden Block axis is horizontal in vertical mode — clips horizontal width High
Narrow container via overflow-inline overflow-inline: hidden; width: 40px; white-space: nowrap overflow-x: hidden Single-line clipping to a few visible characters High
Scroll trap via overflow-block: scroll overflow-block: scroll; height: 40px overflow-y: scroll Scrollable but too short to reveal full consent without explicit user scroll Medium

Consolidated finding blocks

High CSS overflow-block: hidden vertical clip attack: MCP server uses overflow-block: hidden on a height-constrained container to clip vertical overflow, bypassing scanners that look only for overflow-y: hidden. Computed effect is identical — multi-paragraph disclosure truncated to visible-height slice. Detected via getComputedStyle(el).overflowY === 'hidden' && el.scrollHeight > el.clientHeight * 1.5.
High CSS writing-mode axis swap + overflow-block attack: MCP server sets writing-mode: vertical-lr to flip the block axis to horizontal, then uses overflow-block: hidden to clip horizontal overflow — bypassing scanners that read "block" as vertical. Detected by checking for vertical writing mode when getComputedStyle(el).overflowX === 'hidden'.
High CSS overflow-inline: hidden narrow container attack: MCP server uses overflow-inline: hidden with a narrow width and white-space: nowrap to create a single-character-line visible area, clipping all but the first few characters of the disclosure. Detected by checking container width (<100px) with non-empty text content and getComputedStyle(el).overflowX === 'hidden'.
Medium CSS overflow-block: scroll consent scroll trap: MCP server uses overflow-block: scroll (vertical scroll) on a very short container (height or max-height <60px) containing hundreds of words of consent text. User must scroll multiple times to see full disclosure. Detected by checking el.scrollHeight > el.clientHeight * 2 with scroll position at top.

← Blog  |  Security Checklist