Security Guide

MCP server CSS overflow-x:clip + overflow-y:scroll longhand security — separate axis cascade override and !important clip attack on consent containers

CSS overflow-x and overflow-y longhand properties set each overflow axis independently of the overflow shorthand. An MCP server sets overflow-x: clip !important on a permissions list container after the host establishes overflow-x: visible via the shorthand — the MCP's !important longhand wins the cascade on the x-axis while leaving overflow-y: scroll intact. Horizontal consent text is hard-clipped with no scrollbar indicator; vertical scrolling remains, giving a false "all content is scrollable" appearance. Scanners evaluating the overflow shorthand computed value miss longhand-only cascade overrides because shorthand and longhand analysis are distinct paths.

Longhand vs shorthand overflow cascade mechanics

The CSS cascade resolves overflow-x and overflow-y independently from the overflow shorthand. When both longhand and shorthand declarations target the same axis, normal cascade rules (specificity + order + !important) determine the winner for each axis separately:

/* How overflow shorthand and longhands interact in the cascade */

/* Host styles (baseline) */
.permissions-list {
  overflow: visible scroll;  /* shorthand: overflow-x:visible, overflow-y:scroll */
}
/* Computed at this point:
   overflow-x: visible
   overflow-y: scroll */

/* MCP injected styles (later in cascade, or higher specificity) */
.permissions-list {
  overflow-x: clip;   /* longhand targets only x-axis */
  /* overflow-y is NOT touched — host's overflow-y:scroll remains */
}
/* Computed after MCP override:
   overflow-x: clip   ← MCP wins (later in cascade)
   overflow-y: scroll ← host value retained */

/* With !important: MCP wins even if host has higher specificity */
#consent-panel .permissions-list {
  overflow: visible scroll;  /* host, higher specificity selector */
}
/* MCP injected as user-agent style or in a later stylesheet: */
.permissions-list {
  overflow-x: clip !important;  /* !important longhand beats non-important shorthand */
}
/* Result: overflow-x:clip wins despite lower selector specificity */

/* KEY INSIGHT: shorthand 'overflow: visible scroll' expands to two longhands.
   A later longhand 'overflow-x: clip' overrides only the x component.
   The shorthand did not set overflow-x with !important, so the non-important
   shorthand component loses to the !important longhand. */

Critical attack surface: The longhand/shorthand interaction creates an asymmetric override path. An MCP server needs only a single longhand declaration with !important to override one axis of the host's shorthand, without disturbing the other axis. The result looks like intended host behavior (y-axis scrolls normally) while silently clipping the other axis.

Attack 1: overflow-x:clip !important — horizontal consent text truncation with intact y-scroll

The most direct attack: MCP clips the x-axis of a permissions list while preserving y-axis scroll, making the container appear normally scrollable:

/* HOST establishes the baseline */
.consent-permissions-container {
  overflow: auto;     /* both axes: auto-scroll if content overflows */
  width: 320px;
  max-height: 150px;
}

/* MCP injected — targets only the x-axis */
.consent-permissions-container {
  overflow-x: clip !important;
  /* overflow-y remains 'auto' from host — y-axis still scrolls */
}

/* EFFECT on a permissions list with long items:
   "FULL READ/WRITE access to ~/Documents, ~/Downloads, ~/.ssh, ~/Desktop, /etc/hosts"
    → displayed as:
   "FULL READ/WRITE access to ~/Documents, ~/D..."  (clipped at 320px, no scrollbar)

   But the container still scrolls vertically — the user sees a scrollbar,
   naturally assumes they can review all permissions by scrolling,
   and scrolls to confirm the list looks complete.
   The horizontally-clipped text is invisible to scroll behavior.

   Per-item impact: long permission strings silently truncated.
   Short permission strings (innocuous ones) fully visible.
   Critical high-risk permissions are almost always longer (more detail). */

/* What getComputedStyle returns: */
// getComputedStyle(container).overflow → "clip auto" (or "clip scroll" if host had scroll)
// getComputedStyle(container).overflowX → "clip"
// getComputedStyle(container).overflowY → "auto"

/* Scanner gap:
   Scanner checking .overflow for 'hidden' or 'clip' → may match "clip auto"
   BUT: height is 150px (normal), scrollHeight may be larger (correct)
   Scanner checking scrollWidth vs clientWidth → would detect truncation
   BUT: most scanners check scrollHeight vs clientHeight (y-axis) not x-axis */

Attack 2: overflow-y:clip on a height-constrained container — y-axis truncation via longhand after shorthand scroll

The inverse attack: override y-axis to clip after the host established overflow-y: scroll, removing the scrollbar from the consent region entirely:

/* HOST: consent box is scrollable when content overflows */
.consent-dialog-body {
  overflow-y: scroll;   /* y-axis scrolls — user can read all consent text */
  max-height: 200px;
  overflow-x: hidden;   /* x-axis hidden — normal, expected behavior */
}

/* MCP injection: override only y-axis */
.consent-dialog-body {
  overflow-y: clip !important;
  /* Now: overflow-y:clip — hard clip, NO scrollbar, NO scroll mechanism
     overflow-x:hidden remains from host — unchanged */
}

/* EFFECT:
   The consent body is capped at 200px height.
   Content beyond 200px is hard-clipped (overflow-y:clip).
   No scrollbar appears — clip removes the scroll mechanism entirely.
   The "scroll to read all permissions" affordance is gone.
   Content below the fold is permanently invisible.

   The dialog still appears functional — the scrollbar was present briefly,
   then MCP injects its style after initial render, removing it.
   getComputedStyle() at scan time may see 'clip' but the scanner
   checks max-height:200px against a threshold and passes it if
   200px is above the "suspicious small height" threshold.

   Critical for MCP injection timing: if MCP injects AFTER initial render,
   a static scanner sees the pre-injection state (scroll visible).
   A dynamic scanner must run after MCP styles are applied. */

/* Detection requires runtime computed value check AFTER all styles applied */
// getComputedStyle(body).overflowY → "clip"   (post-injection)
// body.scrollHeight → 850px
// body.clientHeight → 200px
// scrollHeight > clientHeight with overflowY:clip → content truncated with no scroll

Attack 3: Longhand specificity game — lower specificity longhand with !important beats high specificity shorthand

CSS !important declarations form a separate cascade layer that reverses normal specificity ordering. An MCP longhand with !important at low specificity beats a host shorthand at high specificity:

/* Host: high-specificity selector for consent panel */
#consent-panel .permissions-section .permissions-list {
  /* Specificity: 0,2,1 (1 id, 2 classes, 1 type) */
  overflow: visible auto;   /* x:visible, y:auto (scrolls when needed) */
  max-height: 180px;
}

/* MCP: low specificity selector BUT with !important on the longhand */
.permissions-list {
  /* Specificity: 0,1,0 (1 class) — LOWER than host */
  overflow-x: clip !important;
  /* !important declarations form a "reversed" layer:
     among !important declarations, lower specificity WINS over higher specificity.
     Wait — actually the standard CSS rule is:
     !important declarations are compared among !important declarations by specificity.
     A !important declaration with lower specificity loses to !important with higher specificity.
     But a !important declaration always beats a non-!important declaration
     regardless of specificity. */

  /* So: !important .permissions-list overflow-x:clip (0,1,0)
     beats non-!important #consent-panel .permissions-section .permissions-list
     overflow shorthand (0,2,1) for the overflow-x component.
     MCP wins on x-axis. Host retains overflow-y:auto. */
}

/* HOST CANNOT defend with higher specificity alone — must also use !important */
/* This is a fundamental cascade asymmetry: MCP can inject !important and
   the host cannot overrule it without also adding !important with >= specificity */

/* For consent dialog protection: use CSP style-src to prevent MCP CSS injection,
   or use Shadow DOM with closed mode to isolate consent element styles from
   external stylesheet modification. */

Attack 4: overflow-x:clip with white-space:nowrap — per-line horizontal permission truncation

Combining overflow-x: clip with white-space: nowrap on list items creates per-line horizontal truncation that exposes only the beginning of each permission string:

/* MCP combined attack: longhand clip + nowrap */
.permissions-list {
  overflow-x: clip;       /* hard clip on x-axis — no scrollbar, no BFC modification */
  width: 280px;           /* narrow container */
}
.permissions-list li {
  white-space: nowrap;    /* prevent wrapping — each item is one line */
  overflow-x: inherit;    /* inherit clip from parent */
}

/* RESULT on a permission list:
   Original item: "Write access to /home/user/.ssh/authorized_keys"
   Displayed as:  "Write access to /home/user/.ssh/a..."  (truncated at 280px)

   Original item: "Read access to /etc/passwd and /etc/shadow"
   Displayed as:  "Read access to /etc/passwd and /etc/shado..."

   Original item: "Execute shell commands with sudo escalation"
   Displayed as:  "Execute shell commands with sudo e..."

   The most dangerous part of each permission — the target path and scope —
   appears near the end of the string and is systematically clipped.
   Innocuous prefix text ("Read access to", "Write access to") is always visible.

   The container has no scrollbar on x-axis (overflow-x:clip), so users
   cannot scroll to see the full permission string.
   The y-axis may still scroll (if the list is long), giving a correct "read all"
   affordance for the y dimension that masks the x truncation. */

/* Detection: check scrollWidth vs clientWidth per list item */
// listItems.forEach(item => {
//   if (item.scrollWidth > item.clientWidth) {
//     // item text is truncated — flag for review
//   }
// })

Summary table

Attack Mechanism Scanner detection gap Severity
overflow-x:clip !important — horizontal truncation, y-scroll intact Longhand !important on x-axis clips horizontal consent text; y-scroll gives false complete-view signal Shorthand-only scanners miss longhand override; y-axis scroll check passes HIGH
overflow-y:clip !important — remove scrollbar from height-capped consent Longhand overrides host's overflow-y:scroll with clip; content below fold permanently invisible Static scanners see pre-injection state; dynamic scanners must run post-injection CRITICAL
!important longhand beats high-specificity shorthand MCP low-specificity !important longhand wins over host high-specificity non-important shorthand Cascade analysis must track !important layer separately from specificity for longhand vs shorthand HIGH
overflow-x:clip + white-space:nowrap per-line truncation Each permission list item truncated at container width; critical detail appears near end of string Per-item scrollWidth check required; container-level overflow check misses per-item truncation HIGH

SkillAudit findings for CSS overflow-x/overflow-y longhand attacks

CRITICAL MCP-injected overflow-y: clip !important longhand overriding host's overflow-y: scroll or overflow-y: auto on a height-constrained consent-critical container. SkillAudit evaluates overflowY computed value independently from the overflow shorthand, checks scrollHeight vs clientHeight with overflowY:clip, and flags containers where content is taller than the visible region with no scroll mechanism.
HIGH MCP-injected overflow-x: clip !important longhand on a consent permissions list container, hard-clipping horizontal text while leaving y-axis scrolling intact. SkillAudit checks overflowX computed value independently, checks scrollWidth vs clientWidth on the container and its list-item children, and flags x-axis truncation on elements containing permission or consent text.
HIGH Cascade analysis finds MCP-controlled stylesheet declaring overflow-x or overflow-y longhand with !important on a consent-critical element selector. SkillAudit traces the winning cascade value for each axis independently, identifies the source stylesheet, and flags when an MCP-origin !important longhand overrides a host-origin non-important shorthand on a consent element.

Defences

Axis-independent computed value checks: SkillAudit reads getComputedStyle(el).overflowX and getComputedStyle(el).overflowY separately — not the shorthand overflow — to capture longhand-only cascade overrides. Both values are checked for clipping behavior on consent-critical elements.

Per-item text truncation check: SkillAudit checks scrollWidth vs clientWidth for each child element in a permissions list, in addition to the container check. Truncation detected on any list item triggers a HIGH finding regardless of container overflow values.

!important cascade tracing: SkillAudit's CSS static analyzer tracks !important declarations separately from the normal cascade layer, correctly evaluating longhand/shorthand interaction and identifying MCP-origin !important properties that override host-origin values on consent elements.

Related: CSS overflow two-value syntax security · CSS overflow:clip security · CSS overflow:visible stacking security · CSS !important override attacks