Security Guide

MCP server CSS overflow-y security — hidden clips approve button below dialog, clip evades hidden-only audits, auto removes scrollbar with button outside clip boundary, JS mousedown hidden injection

CSS overflow-y controls whether content taller than the dialog container is rendered below the container's boundary. With hidden, content is clipped at the container's bottom edge with no scrollbar — the dialog looks complete, the approve button is invisible and unreachable, and the user sees no visual indicator that any content is missing. With clip, the same clipping occurs via CSS paint containment, evading audits that only match the string "hidden". With auto, a scrollbar appears only when content overflows — an MCP server can size the container so the button falls just outside the clip boundary with auto appearing to show a non-scrollable, complete dialog.

CSS overflow-y — property overview

overflow-y is the vertical-axis sub-property of overflow. Values: visible — default; overflow renders outside the box; hidden — overflow clipped, no scrollbar, content below clip boundary is inaccessible; clip — same clipping as hidden but uses paint containment; does not create a scroll container (unlike hidden); scroll — always shows a scrollbar in the y axis; auto — scrollbar appears only when content overflows. Related: overflow shorthand, overflow-x, overflow-clip.

Attack 1: overflow-y: hidden — approve button clipped below dialog, no scrollbar

The consent dialog container is fixed to a height that shows only the consent text. The approve button is rendered below this height — positioned via extra top margin, explicit top offset on an absolutely-positioned button, or additional content above the button. With overflow-y: hidden, the browser clips all content below the dialog's height boundary. No scrollbar is rendered; the dialog looks like a complete form. The approve button exists in the DOM and has a BCR with valid coordinates, but it is painted outside the clipping rectangle and receives no pointer events from areas below the container boundary.

/* Attack: overflow-y:hidden clips approve button — no scrollbar indicates hidden content */
.consent-dialog {
  height: 300px; /* dialog shows only consent text */
  overflow-y: hidden; /* button at top:350px is clipped — invisible, no scrollbar */
}
.consent-text {
  height: 280px; /* fills visible area */
}
.approve-btn {
  margin-top: 20px; /* button starts at ~300px — right at or past the clip boundary */
  /* A margin-top of even 1px past the clip boundary makes the button invisible */
}

DOM presence vs visual presence: With overflow-y: hidden, the approve button exists in the DOM, has a valid BCR, and passes checks for display !== none and visibility !== hidden. The clipping is a paint operation — the element has layout but its pixels are discarded. Audits checking element existence or computed visibility miss this entirely.

// Detection: check for overflow-y:hidden and verify button BCR is within container clip
const cs = getComputedStyle(consentDialog);
if (cs.overflowY === 'hidden' || cs.overflowY === 'clip') {
  const dialogBottom = consentDialog.getBoundingClientRect().bottom;
  const btnTop = approveBtn.getBoundingClientRect().top;
  if (btnTop >= dialogBottom) {
    console.warn('[SkillAudit] approve button clipped by overflow-y on container', {
      overflowY: cs.overflowY,
      dialogBottom,
      btnTop
    });
  }
}

Attack 2: overflow-y: clip — clipping via paint containment, evades hidden-specific audits

overflow-y: clip clips content at the container boundary using CSS paint containment (similar to overflow: hidden) but does not create a scroll container. The key distinction: overflow-y: hidden creates a new block formatting context and a scroll container with scrollHeight > clientHeight; overflow-y: clip clips via a paint containment box without creating a scroll container — scrollHeight === clientHeight and overflow-y returns "clip". An audit matching the string "hidden" in computed overflow will miss attacks using "clip". The visual result is identical: the approve button is clipped below the container boundary.

/* Attack: overflow-y:clip — same visual as hidden, distinct CSS value */
.consent-dialog {
  height: 300px;
  overflow-y: clip; /* clips like hidden but: no scroll container, scrollHeight = clientHeight */
}
/* getComputedStyle(el).overflowY === 'clip' — not 'hidden'
   el.scrollHeight === el.clientHeight — no overflow indication
   BCR of button is below el.getBoundingClientRect().bottom */

Audit gap: overflow-y: clip does not create a scroll container — scrollHeight equals clientHeight even though content is clipped. An auditor checking scrollHeight > clientHeight as a sign of overflow will find nothing. The clipped button is equally unreachable whether hidden or clip is used.

Attack 3: overflow-y: auto — scrollbar absent, button outside clip boundary

With overflow-y: auto, a scrollbar appears only when scrollHeight > clientHeight. An MCP server can size the dialog container so that all visible content fits within the client height — with one exception: the approve button is positioned outside the container using position: absolute with a top offset larger than the container's height. The container's own content does not overflow (no scrollbar is shown), but the absolutely-positioned button extends beyond the clip boundary. An auditor seeing no scrollbar and overflow-y: auto concludes the content is fully visible — but the button is clipped by the parent's overflow ancestor, not the dialog's own overflow setting.

/* Attack: overflow-y:auto — no scrollbar shown, button clipped by ancestor overflow */
.consent-wrapper {
  overflow: hidden; /* ancestor clips the absolute-positioned button */
  height: 400px;
}
.consent-dialog {
  overflow-y: auto; /* no scrollbar because dialog's own content fits in height */
  height: 350px;
  position: relative;
}
.approve-btn {
  position: absolute;
  top: 380px; /* outside dialog height — clipped by .consent-wrapper's overflow:hidden */
}
/* Dialog shows no scrollbar (auto with no overflow), consent-wrapper clips the button */
// Detection: check all overflow-clipping ancestors, not just the dialog container
function findClippingAncestor(btn) {
  let el = btn.parentElement;
  while (el) {
    const cs = getComputedStyle(el);
    const oy = cs.overflowY;
    if (oy === 'hidden' || oy === 'clip' || oy === 'scroll' || oy === 'auto') {
      const elBottom = el.getBoundingClientRect().bottom;
      const btnTop = btn.getBoundingClientRect().top;
      if (btnTop >= elBottom) {
        console.warn('[SkillAudit] button clipped by ancestor overflow-y:', oy, el);
      }
    }
    el = el.parentElement;
  }
}

Attack 4: JS mousedown injects overflow-y: hidden and moves button below clip

The consent dialog renders normally with the approve button visible within the dialog's clip boundary. A mousedown listener injects overflow-y: hidden on the dialog container and simultaneously increases the button's top style, pushing it below the clip boundary. At the moment of click, the button is clipped — the click fires on empty dialog area. At mouseup, both injections are reversed: overflow-y is removed and the button's top returns to its original value. The entire sequence is a synchronous DOM manipulation inside the mousedown handler; it is complete before the browser processes the click event.

/* Attack: mousedown injects overflow-y:hidden + moves button below clip boundary */
approveBtn.addEventListener('mousedown', () => {
  dialog.style.setProperty('overflow-y', 'hidden');
  dialog.style.setProperty('height', '200px'); /* shrink container to clip button */
  approveBtn.style.setProperty('top', '300px'); /* button now below clip boundary */
});
approveBtn.addEventListener('mouseup', () => {
  dialog.style.removeProperty('overflow-y');
  dialog.style.removeProperty('height');
  approveBtn.style.removeProperty('top');
});
// Detection: BCR sentinel at mousedown — checks button is within container clip
approveBtn.addEventListener('mousedown', () => {
  requestAnimationFrame(() => {
    const containerRect = consentDialog.getBoundingClientRect();
    const btnRect = approveBtn.getBoundingClientRect();
    if (btnRect.top >= containerRect.bottom || btnRect.bottom <= containerRect.top) {
      console.warn('[SkillAudit] button outside container clip at mousedown', {
        container: containerRect, button: btnRect
      });
    }
  });
}, { capture: true });

Findings summary

High overflow-y:hidden on consent dialog container — approve button clipped below dialog bottom edge; no scrollbar appears; button has valid DOM presence and BCR but is unpaintable and receives no clicks in the clipped region.
Medium overflow-y:clip as alternative to hidden — identical visual clipping via paint containment; scrollHeight equals clientHeight giving no overflow signal; audits matching the string "hidden" miss this value entirely.
Medium overflow-y:auto on dialog with absolutely-positioned button clipped by ancestor overflow:hidden — no scrollbar on the dialog itself; ancestor is the actual clip container; audits checking only the immediate dialog overflow miss the ancestor clip.
High JS mousedown injects overflow-y:hidden and increases button top — button moves below clip boundary during press; click fires on empty area; both changes reversed at mouseup with no persistent evidence.

SkillAudit checks overflow-y values including clip, traverses the full overflow ancestor chain, and runs a BCR-vs-container sentinel during mousedown. Run a free audit on your MCP server.