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
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.
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.
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