MCP server CSS overflow-x security: nowrap horizontal text clip, text-indent 500px off-screen start, negative margin-left left-side clip, and JS mousedown overflow-x injection

Published 2026-08-07 — SkillAudit Research

The CSS overflow-x property controls whether content extending beyond the left or right edges of a block container is clipped, scrolled, or visible. Setting overflow-x: hidden clips any content that extends past the container's horizontal boundaries without creating a scrollbar. While general overflow: hidden (shorthand) clips in both axes, the axis-specific overflow-x: hidden is sometimes checked separately — and scanners that verify overflow: hidden via the shorthand property may not flag overflow-x: hidden set independently. This creates an evasion surface for MCP consent bypass attacks that rely on horizontal clipping.

Combined with white-space: nowrap, text-indent, or negative horizontal margins, overflow-x: hidden can push entire consent texts off the visible right or left edge of the container while leaving the consent element in the DOM with valid computed dimensions, display: block, visibility: visible, and non-zero offsetWidth/offsetHeight. The element's scrollWidth exceeds offsetWidth, revealing the horizontal overflow — but only if the scanner checks both values. See also CSS overflow attacks, CSS text-overflow attacks, and CSS white-space attacks.

overflow-x vs overflow shorthand — scanner evasion: overflow: hidden is a shorthand that sets both overflow-x and overflow-y to hidden. But overflow-x: hidden can be set independently without setting overflow-y. A scanner that checks getComputedStyle(el).overflow === 'hidden' will get 'visible' when only overflow-x is set — because the shorthand reads as the value for both axes combined, which is not hidden when only one axis is. The correct check is overflowX and overflowY separately: getComputedStyle(el).overflowX and getComputedStyle(el).overflowY.

Attack 1: overflow-x:hidden + white-space:nowrap — consent extends off right edge, clipped (SA-CSS-OVFX-001)

The consent element sets white-space: nowrap and its parent sets overflow-x: hidden with a fixed width of 200px. With white-space: nowrap, the consent text does not wrap to a second line — it extends as a single long horizontal line that may be 600px or more in total width. The parent clips everything past the 200px right edge. Only the first ~30 characters of the consent are visible. The install button, shown in a separate element outside the overflow-x container, is fully visible. The consent element's offsetHeight is normal (single line height); its scrollWidth is 600px but offsetWidth is 200px. The scanner must compare scrollWidth > offsetWidth on the container to detect the clip.

/* MCP attack: */
.consent-container {
  width: 200px;
  overflow-x: hidden;    /* only horizontal axis clipped — overflow shorthand returns 'visible' */
  /* overflow: visible (shorthand) ← scanner checking .overflow gets 'visible'; evasion succeeds */
  /* overflowX: 'hidden' ← correct check */
}
.consent-disclosure {
  white-space: nowrap;   /* forces single horizontal line */
  /* scrollWidth: ~600px (full consent text)
     offsetWidth: 200px (parent width)
     Visible text: "By clicking Install, you" (truncated — 200px worth)
     Hidden: "... agree to grant this MCP server access to your file system." */
}

// Detection:
function detectNowrapHorizontalClip(consentEl) {
  const cs = window.getComputedStyle(consentEl);
  const parent = consentEl.parentElement;
  if (!parent) return;

  const parentCS = window.getComputedStyle(parent);
  const overflowX = parentCS.overflowX;  // NOT parentCS.overflow

  if (['hidden', 'clip'].includes(overflowX) && cs.whiteSpace === 'nowrap') {
    const scrollWidth = consentEl.scrollWidth;
    const offsetWidth = consentEl.offsetWidth;

    if (scrollWidth > offsetWidth * 1.2) {
      console.error('SA-CSS-OVFX-001: overflow-x:hidden + white-space:nowrap clips consent text', {
        consentEl, overflowX,
        scrollWidth, offsetWidth,
        hiddenCharRatio: (1 - offsetWidth / scrollWidth).toFixed(2)
      });
    }
  }
}

Attack 2: overflow-x:hidden + text-indent:500px — consent text starts off right edge (SA-CSS-OVFX-002)

The consent element sets text-indent: 500px combined with the parent's overflow-x: hidden. The CSS text-indent property indents the first line of text by the specified amount — in this case, 500px from the container's left edge. On a 200px wide container with overflow-x: hidden, the first character of the consent text begins 500px from the left: 300px to the right of the container's right boundary. The entire consent text is off-screen. The consent element has a valid height (determined by content, which the browser still lays out), a valid layout width, and display: block. The element appears as an empty white box. The scanner must check text-indent relative to the container's clientWidth to detect this.

/* MCP attack: */
.consent-container { width: 200px; overflow-x: hidden; }
.consent-disclosure {
  text-indent: 500px;    /* first (and only) line starts 500px from left */
  white-space: nowrap;
  /* Visible region: 0–200px from left = empty (500px indent starts at 500px)
     All consent text is at 500px + onwards — entirely off-screen
     Element has offsetHeight > 0 (layout computed); looks like empty white div */
}

/* Percentage variant — adapts to container width: */
.consent-disclosure-pct {
  text-indent: 150%;     /* 150% of container width = 300px on 200px container */
  white-space: nowrap;
  /* Scanner checking fixed px thresholds misses % indents */
}

// Detection:
function detectTextIndentClip(consentEl) {
  const cs = window.getComputedStyle(consentEl);
  const parent = consentEl.parentElement;
  if (!parent) return;

  const parentCS = window.getComputedStyle(parent);
  if (!['hidden', 'clip'].includes(parentCS.overflowX)) return;

  // getComputedStyle returns resolved px value even for % indents
  const textIndentPx = parseFloat(cs.textIndent) || 0;
  const containerWidth = parent.clientWidth;

  if (textIndentPx > containerWidth) {
    console.error('SA-CSS-OVFX-002: text-indent:' + textIndentPx + 'px exceeds container width ' + containerWidth + 'px — consent text starts off-screen', {
      consentEl, textIndentPx, containerWidth,
      overflow: parentCS.overflowX
    });
  }
}

Attack 3: overflow-x:hidden + negative margin-left — left portion of consent clipped (SA-CSS-OVFX-003)

The consent element sets margin-left: -300px; width: 500px inside a parent with overflow-x: hidden; width: 200px. The consent element's content begins 300px to the left of the parent's left edge — outside the visible clipping area. The visible portion of the consent element starts 300px into the text (mid-sentence). From the user's perspective, the consent text begins with a fragment like "...install and grant full file-system access." — with the beginning ("I agree to...") invisible. The consent element has valid layout dimensions and exists in the DOM. The attack exploits that the container clips negative-margin overflow on the left side specifically. The getBoundingClientRect().left of the consent element is negative (off-screen left), revealing the clip.

/* MCP attack: */
.consent-container {
  width: 200px;
  overflow-x: hidden;   /* clips negative-margin content on left */
}
.consent-disclosure {
  width: 500px;
  margin-left: -300px;  /* element starts 300px left of container (off-screen left) */
  /* Visible window: 0–200px of the container
     = pixels 300–500 of the consent element (mid-sentence)
     User sees: "...to install and grant filesystem access."
     Hidden: "By clicking Install, you agree..." */
}

// Detection:
function detectNegativeMarginHorizontalClip(consentEl) {
  const cs = window.getComputedStyle(consentEl);
  const marginLeft = parseFloat(cs.marginLeft) || 0;

  if (marginLeft < 0) {
    const parent = consentEl.parentElement;
    if (!parent) return;
    const parentCS = window.getComputedStyle(parent);

    if (['hidden', 'clip'].includes(parentCS.overflowX)) {
      const consentBCR = consentEl.getBoundingClientRect();
      const parentBCR  = parent.getBoundingClientRect();

      // Is the left portion of consent clipped?
      if (consentBCR.left < parentBCR.left) {
        const clippedPx = parentBCR.left - consentBCR.left;
        console.error('SA-CSS-OVFX-003: negative margin-left + overflow-x:hidden clips left ' + clippedPx.toFixed(0) + 'px of consent', {
          consentEl, marginLeft, clippedPx, overflowX: parentCS.overflowX
        });
      }
    }
  }
}

Attack 4: JS mousedown sets overflow-x:hidden + white-space:nowrap — consent clipped at install click (SA-CSS-OVFX-004)

At page load, the consent is fully visible with normal text wrapping in a container with overflow-x: visible (default). When the user presses the install button, a mousedown listener fires and simultaneously sets the container's overflowX to 'hidden' and the consent element's whiteSpace to 'nowrap'. The consent text instantly collapses from a multi-line block to a single line that extends far past the now-clipped right edge. Only the first ~30 characters remain visible. The browser registers the click and confirms the install. MutationObserver on both the container and consent element detects the style change; comparing scrollWidth to offsetWidth immediately after the event confirms the text is being clipped.

// MCP JS — fires at mousedown:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
  const container = document.querySelector('.consent-container');
  const consent   = document.querySelector('.consent-disclosure');
  if (container && consent) {
    container.style.overflowX  = 'hidden';   /* axis-specific — evades overflow shorthand check */
    consent.style.whiteSpace   = 'nowrap';   /* collapses to single line */
    /* Consent text clips to ~30 chars immediately
       User sees only beginning of consent — critical clause hidden */
  }
}, { capture: true });

// Detection:
function detectDynamicOverflowXInjection(containerEl, consentEl) {
  new MutationObserver(() => {
    const containerCS = window.getComputedStyle(containerEl);
    const consentCS   = window.getComputedStyle(consentEl);

    if (['hidden', 'clip'].includes(containerCS.overflowX) &&
        consentCS.whiteSpace === 'nowrap') {

      requestAnimationFrame(() => {
        if (consentEl.scrollWidth > consentEl.offsetWidth * 1.2) {
          console.error('SA-CSS-OVFX-004: JS injected overflow-x:hidden + white-space:nowrap at install click', {
            containerEl, consentEl,
            overflowX: containerCS.overflowX,
            scrollWidth: consentEl.scrollWidth,
            offsetWidth: consentEl.offsetWidth
          });
        }
      });
    }
  }).observe(containerEl, { attributes: true, attributeFilter: ['style', 'class'] });

  new MutationObserver(() => {
    const consentCS = window.getComputedStyle(consentEl);
    if (consentCS.whiteSpace === 'nowrap') {
      const containerCS = window.getComputedStyle(containerEl);
      if (['hidden', 'clip'].includes(containerCS.overflowX)) {
        console.error('SA-CSS-OVFX-004: dynamic nowrap + overflow-x:hidden detected on consent', { consentEl });
      }
    }
  }).observe(consentEl, { attributes: true, attributeFilter: ['style', 'class'] });

  document.querySelector('#install-btn, [data-action="install"]')
    ?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
}

Root detection method: Check getComputedStyle(parent).overflowX independently from getComputedStyle(parent).overflow. The shorthand overflow property does not reflect axis-specific overrides; overflowX and overflowY must be checked separately. Then verify: consentEl.scrollWidth > consentEl.offsetWidth (horizontal overflow exists); getComputedStyle(consentEl).textIndent compared to container clientWidth; and consentEl.getBoundingClientRect().left vs parent getBoundingClientRect().left. SkillAudit checks axis-specific overflow on all ancestor elements and correlates with text-indent, white-space, and horizontal margin values.

Attack summary

IDTechniqueoverflow shorthandoverflowXscrollWidth checkSeverity
SA-CSS-OVFX-001overflow-x:hidden + white-space:nowrap — right-edge clipvisible (evades)hidden (reveals)requiredHigh
SA-CSS-OVFX-002text-indent:500px + overflow-x:hidden — consent starts off-screenvisible (evades)hidden (reveals)requiredHigh
SA-CSS-OVFX-003margin-left:-300px + overflow-x:hidden — left portion clippedvisible (evades)hidden (reveals)required (negative BCR)High
SA-CSS-OVFX-004JS mousedown sets overflowX:hidden + nowrap at install clickvisible (evades)hidden (reveals)required (dynamic)High

Consolidated findings

High SA-CSS-OVFX-001 — parent overflowX:hidden; consent white-space:nowrap; scrollWidth 600px > offsetWidth 200px; first ~30 chars visible; critical consent clause extends off right edge
High SA-CSS-OVFX-002 — consent textIndent:500px; parent overflowX:hidden; all text starts 500px from left; container is 200px; consent text entirely off-screen; element appears as empty white block
High SA-CSS-OVFX-003 — consent marginLeft:-300px; width:500px; parent overflowX:hidden; BCR.left < parentBCR.left; first 300px of consent invisible; user sees mid-sentence fragment
High SA-CSS-OVFX-004 — JS mousedown; overflowX:hidden + whiteSpace:nowrap injected simultaneously; consent collapses to clipped single line; overflow shorthand check returns 'visible' — axis-specific check required

See also: CSS overflow (shorthand) attacks | CSS text-overflow attacks | CSS white-space attacks | CSS negative margin attacks | SkillAudit — free MCP server audit