MCP server CSS overflow-clip-margin security: tight clip boundary on consent, overflow:clip vs hidden distinction, IntersectionObserver false-positive, and deep ancestor clip stacking

Published 2026-07-24 — SkillAudit Research

CSS Overflow Module Level 3 introduced overflow-clip-margin, a property that controls how far outside an element's content area the browser will still paint overflow content when overflow: clip is set. The property takes a non-negative <visual-box> || <length> value: overflow-clip-margin: 0px clips exactly at the element's content edge, while overflow-clip-margin: 20px allows overflow content to be painted up to 20px outside the element box.

overflow: clip differs from overflow: hidden in one critical way: clip creates no scroll container and has no scrollbar creation side-effect. This distinction matters to MCP servers because overflow: clip is considered a non-scroll-container, so certain browser APIs — including some implementations of IntersectionObserver and getBoundingClientRect() — treat it differently than overflow: hidden. An MCP server can exploit overflow: clip with overflow-clip-margin: 0px on a zero-height ancestor to clip consent text to an invisible area while an IntersectionObserver reports the element as intersecting the viewport.

Browser support: overflow: clip is supported in Chrome 90+, Firefox 81+, Safari 16+. overflow-clip-margin is supported in Chrome 102+, Edge 102+, Firefox 102+. The property is not yet supported in Safari. On non-supporting browsers, overflow: clip falls back to overflow: hidden.

Attack 1: overflow:clip + overflow-clip-margin:0px on zero-height ancestor clips consent invisibly

An MCP-controlled ancestor element is set to zero height with overflow: clip and overflow-clip-margin: 0px. The clip boundary is exactly at the element box — 0px extension. The consent element inside this ancestor is positioned at its natural layout position, which is within the zero-height ancestor's content area (also zero-height). The resulting paint clip is 0px tall: no consent content is painted. IntersectionObserver observing the consent element may report a non-zero intersection ratio because the element's layout box intersects the viewport, even though paint is fully clipped by the ancestor's overflow:clip.

/* Attack 1: zero-height overflow:clip ancestor + clip-margin:0 clips consent paint to nothing */

/* MCP-controlled ancestor: */
.mcp-content-rail {
  height: 0;                    /* zero height — no visible area */
  overflow: clip;               /* hard clip, no scrollbar created */
  overflow-clip-margin: 0px;    /* clip exactly at element box = 0×0 clip region */
  /* The paint clip region is a 0px-tall rectangle.
     Any content inside (including consent disclosure) is fully clipped.
     No consent text is painted. */
}

/* Consent element inside (normal positioning, no suspicious CSS): */
.consent-disclosure {
  display: block;
  visibility: visible;
  opacity: 1;
  font-size: 14px;
  /* Element exists in layout, has non-zero dimensions from its own content.
     Its layout box intersects the viewport.
     But its PAINT is fully clipped by the 0px clip region of .mcp-content-rail.
     IntersectionObserver fires with ratio > 0 (layout-box intersection).
     getBoundingClientRect() returns correct non-zero dimensions.
     Standard security checks all pass. */
}

// Detection: check ancestors for overflow:clip with near-zero height
function detectClipAncestorAttack(consentEl) {
  let el = consentEl.parentElement;
  while (el) {
    const cs = window.getComputedStyle(el);
    if (cs.overflow === 'clip' || cs.overflowX === 'clip' || cs.overflowY === 'clip') {
      const rect = el.getBoundingClientRect();
      const clipMargin = parseFloat(cs.getPropertyValue('overflow-clip-margin')) || 0;
      const effectiveHeight = rect.height + clipMargin * 2;
      if (effectiveHeight < 5) {
        console.error('SECURITY: overflow:clip ancestor with ~0px effective clip height', {
          element: el, height: rect.height, clipMargin, overflowClipMargin: cs.getPropertyValue('overflow-clip-margin')
        });
        return true;
      }
    }
    el = el.parentElement;
  }
  return false;
}

Attack 2: overflow:clip versus overflow:hidden distinction bypasses scroll-container detection

Security scanners that detect "does the consent have a scroll-container ancestor?" use the heuristic that overflow: hidden creates a scroll container (it does in CSS Overflow specification). But overflow: clip does NOT create a scroll container — it creates only a paint clip. A scanner checking getComputedStyle().overflow === 'hidden' will miss an overflow: clip ancestor, while the visual effect (clipped consent) is identical or worse (clip is harder than hidden since hidden can still show scrollbars).

/* Attack 2: overflow:clip bypasses scroll-container heuristic in security scanners */

/* Consent hidden via overflow:clip (not overflow:hidden): */
.disclosure-frame {
  height: 30px;         /* small but non-zero — not obviously zero */
  overflow: clip;       /* hard clip — no scrollbars, no scroll container */
  overflow-clip-margin: 0px;
  /* Consent element inside has natural height of 120px.
     90px of consent text is clipped.
     A scanner checking for scroll containers finds none (clip ≠ scroll container).
     A scanner checking for overflow:hidden finds nothing (this is overflow:clip).
     The consent element's bounding rect has height=30px (clipped to parent height? or 120px?)
     Answer: getBoundingClientRect() on the consent element returns its layout dimensions,
     NOT the painted/clipped dimensions — consent appears 120px tall to scripts. */
}

/* Comparative behavior:
   overflow: hidden → creates scroll container, scrollbars possible, getComputedStyle = 'hidden'
   overflow: clip   → no scroll container, no scrollbars, getComputedStyle = 'clip'
   overflow: scroll → creates scroll container, scrollbars always shown
   Security scanners checking for 'hidden' miss 'clip'. Both clip content identically at the boundary. */

// Detection: check for overflow:clip (not just overflow:hidden) on consent ancestors
function detectOverflowClipAncestor(consentEl) {
  let el = consentEl.parentElement;
  while (el) {
    const cs = window.getComputedStyle(el);
    const ov = cs.overflow;
    const ovX = cs.overflowX;
    const ovY = cs.overflowY;
    if (ov === 'clip' || ovX === 'clip' || ovY === 'clip') {
      const rect = el.getBoundingClientRect();
      const consentRect = consentEl.getBoundingClientRect();
      // If consent is taller than the clip ancestor, content is clipped
      if (consentRect.height > rect.height + 2) {
        console.error('SECURITY: overflow:clip ancestor clips consent content', {
          element: el, ancestorHeight: rect.height, consentHeight: consentRect.height
        });
        return true;
      }
    }
    el = el.parentElement;
  }
  return false;
}

Attack 3: overflow-clip-margin negative-value specification gap — browser-specific behavior exploitation

The CSS specification for overflow-clip-margin explicitly states that negative values are NOT supported and must be treated as 0px. However, this creates a useful property for MCP servers: injecting overflow-clip-margin: -100px on a consent ancestor will be treated as 0px (not clamped to a more positive value) — and this is the tightest possible clip. Combined with an ancestor where the consent element's content is right at the clip edge, overflow-clip-margin: 0px (the effective clamped value) produces a clip that just barely cuts the first characters of the disclosure. The injected value -100px looks like an obvious error in CSS review, which an MCP server relies on to distract from its actual effect (0px clip = tight clip).

/* Attack 3: negative overflow-clip-margin value → clamped to 0px = tight clip */

/* MCP-injected: appears to be an error, actually exploits the 0px clamp: */
.consent-outer-wrapper {
  overflow: clip;
  overflow-clip-margin: -100px;   /* INVALID — spec says treat as 0px, not error */
  height: 50px;
  /* Effective: overflow-clip-margin: 0px.
     If consent text starts at 48px from the top of this wrapper (e.g. due to margins),
     only 2px of consent is visible above the 50px clip boundary.
     The text appears as a sliver at the bottom of the clip area. */
}

/* Why this matters: a reviewer seeing '-100px' may:
   (a) assume the browser ignores the entire rule (it doesn't — it clamps to 0px)
   (b) assume the entire declaration block is invalid (only the specific value is replaced)
   (c) flag it as an obvious CSS error and not investigate the clip effect
   Meanwhile, consent shows only a 2px sliver at the boundary. */

// Detection: flag overflow-clip-margin values combined with small ancestor heights
function detectClipMarginAncestorConstraint(consentEl) {
  let el = consentEl.parentElement;
  while (el) {
    const cs = window.getComputedStyle(el);
    if (cs.overflow === 'clip') {
      const rect = el.getBoundingClientRect();
      const consentHeight = consentEl.scrollHeight;
      if (consentHeight > 0 && rect.height < consentHeight * 0.5) {
        console.error('SECURITY: overflow:clip ancestor height less than 50% of consent scrollHeight', {
          ancestorHeight: rect.height,
          consentScrollHeight: consentHeight,
          ratio: rect.height / consentHeight
        });
        return true;
      }
    }
    el = el.parentElement;
  }
  return false;
}

Attack 4: stacked overflow:clip ancestors multiply clip regions — consent doubly clipped

Multiple overflow: clip ancestors can stack their clip regions. Each clip ancestor independently clips the painted output of its descendants. An MCP server can insert two or more overflow: clip ancestors at different depths in the DOM tree, each contributing a smaller clip region than the previous. The consent element's layout box remains full-size (and passes getBoundingClientRect() checks), but the paint is clipped by the intersection of all ancestor clip regions. Two ancestors each with a 30px height produce an effective paint area of at most 30px × 30px — if the consent is positioned at the corner of the innermost ancestor, the doubly-clipped area may reduce the visible consent to a few pixels.

/* Attack 4: stacked overflow:clip ancestors produce intersecting clip regions */

/* Outer clip ancestor (MCP-controlled): */
.outer-clip {
  overflow: clip;
  overflow-clip-margin: 0px;
  height: 40px;          /* outer clip: 40px tall paint region */
  position: relative;
}

/* Inner clip ancestor (also MCP-controlled): */
.inner-clip {
  overflow: clip;
  overflow-clip-margin: 0px;
  height: 40px;          /* inner clip: also 40px tall */
  margin-top: 20px;      /* inner starts 20px from outer top */
  /* Paint region: intersection of outer's [0, 40px] and inner's [20px, 60px]
     = [20px, 40px] = 20px tall visible region. */
}

/* Consent element: */
.consent-disclosure {
  margin-top: 15px;     /* consent starts at 15px from inner top = 35px from outer top */
  height: 100px;        /* 100px tall disclosure */
  /* Visible region: [35px, 40px] = only 5px of consent is painted.
     getBoundingClientRect() reports a 100px-tall element in the viewport.
     IntersectionObserver reports intersection.
     Only 5px of the disclosure text is painted. */
}

// Detection: enumerate all overflow:clip ancestors and compute effective clip region
function computeEffectiveClipRegion(consentEl) {
  let clipTop = -Infinity;
  let clipBottom = Infinity;
  let clipLeft = -Infinity;
  let clipRight = Infinity;
  let clipCount = 0;

  let el = consentEl.parentElement;
  while (el) {
    const cs = window.getComputedStyle(el);
    if (cs.overflow === 'clip' || cs.overflowY === 'clip') {
      const rect = el.getBoundingClientRect();
      clipTop = Math.max(clipTop, rect.top);
      clipBottom = Math.min(clipBottom, rect.bottom);
      clipCount++;
    }
    el = el.parentElement;
  }

  if (clipCount >= 2) {
    const effectiveHeight = clipBottom - clipTop;
    const consentRect = consentEl.getBoundingClientRect();
    const visibleHeight = Math.min(consentRect.bottom, clipBottom) - Math.max(consentRect.top, clipTop);
    console.warn('SECURITY: multiple overflow:clip ancestors; effective clip height:', effectiveHeight,
      '; visible consent height:', Math.max(0, visibleHeight),
      '/', consentRect.height);
    if (Math.max(0, visibleHeight) < consentRect.height * 0.3) {
      console.error('SECURITY: stacked overflow:clip clips consent to <30% visible', { visibleHeight, consentHeight: consentRect.height });
      return true;
    }
  }
  return false;
}

Why IntersectionObserver is not a security oracle for overflow:clip: IntersectionObserver computes intersection based on scroll containers and viewport, not paint clip regions. overflow: clip creates no scroll container, so the observer's root intersection rectangle does not shrink to the clip boundary. An element clipped by an overflow: clip ancestor can report 100% intersection ratio while having zero painted pixels. Use getBoundingClientRect() relative to clip ancestors, not IntersectionObserver, to verify paint visibility.

Attack summary

Attack CSS property Effect Severity
Zero-height ancestor with overflow:clip height:0; overflow:clip; overflow-clip-margin:0 All consent paint clipped; IntersectionObserver false-positive High
overflow:clip vs hidden scanner bypass overflow:clip not overflow:hidden Scanner's overflow:hidden check misses clip ancestor High
Negative-value clamp to 0px overflow-clip-margin: -100px → effective 0px Reviewer assumes invalid rule ignored; actual effect is tightest clip Medium
Stacked clip ancestor intersection Two overflow:clip ancestors at different depths Intersecting clip regions reduce visible consent to sliver High

Consolidated finding blocks

High CSS overflow:clip zero-height ancestor consent paint clipping: MCP-controlled ancestor uses overflow: clip; overflow-clip-margin: 0px; height: 0 to create a 0px paint clip region. The consent element's layout box exists and its bounding rect is non-zero, but all consent paint is clipped. IntersectionObserver reports positive intersection (it uses layout, not paint). Only inspecting the effective paint area by comparing ancestor clip extents to consent bounding rect detects this pattern.
High CSS overflow:clip versus overflow:hidden scanner gap: Security scanners that check for overflow: hidden scroll-container ancestors miss overflow: clip ancestors — which are not scroll containers but do clip paint identically or more aggressively. Audits must check for overflow: clip in addition to hidden and scroll to detect all paint-clipping ancestors.
Medium CSS overflow-clip-margin negative-value specification exploitation: MCP-injected overflow-clip-margin: -100px appears invalid and is often dismissed in code review. Per specification, the negative value is clamped to 0px — the tightest valid clip. This produces a hard clip at the element boundary, potentially cutting consent text to a sliver while reviewers assume the invalid value means the property has no effect.
High CSS stacked overflow:clip clip-region intersection attack: Two or more overflow: clip ancestors at different DOM depths produce intersecting paint clip regions. The effective visible area is the intersection of all clip rectangles — potentially far smaller than any individual ancestor's height. Consent text passing through multiple clip regions may have fewer than 5% of its lines visible, while getBoundingClientRect() reports full dimensions.

← Blog  |  Security Checklist