Security Guide
MCP server CSS overflow-x: clip + overflow-y: auto security — silent horizontal clipping in scrollable consent containers
CSS overflow-x: clip (CSS Overflow Level 4) differs from overflow-x: hidden in one critical way: clip does not establish a scroll container. Combined with overflow-y: auto, the result is a box that scrolls vertically — showing a scrollbar that implies all content is reachable — but silently clips any horizontal overflow without a scrollbar or indicator. An MCP server uses this combination to hide wide consent text: tables, URLs, long phrases that wrap past the container edge disappear without any visual cue.
The overflow-x:clip vs overflow-x:hidden distinction
The CSS Overflow Level 4 specification introduced the clip value for the overflow properties. Unlike hidden, clip does not establish an overflow formatting context or create a new scroll container. This has several consequences:
- No scroll container: Elements with
overflow-x: clipdo not create a scrollable region.scrollLeftis always 0. The content is simply paint-clipped at the element's edge. - No scrollbar: Since there is no scroll container, no scrollbar appears for the clipped axis — even when content overflows. The clipping is visually silent.
- JavaScript detection difference:
element.scrollWidthstill reports the full content width (including clipped content) — but there is no mechanism to scroll to it.overflow-x: hiddenalso has this behavior, butclipadditionally prevents sticky positioning from escaping the box on that axis.
Browser support: Chrome 90+, Firefox 81+, Safari 16+ — approximately 85% of browsers in 2026.
/* Critical difference */
overflow-x: hidden; /* Creates a scroll container — JS can scroll to x > 0 */
overflow-x: clip; /* Does NOT create a scroll container — content is paint-clipped only */
/* When combined with overflow-y: auto: */
.container {
overflow-x: clip; /* silent horizontal clip — no scroll container on x-axis */
overflow-y: auto; /* vertical scroll container — scrollbar appears when needed */
}
/* Result:
- Vertical scrollbar visible if content is taller than container ✓ (reassuring)
- Horizontal content that overflows is invisibly cut ✗ (no indicator)
- User sees scrollbar and believes all content is accessible by scrolling
- Wide text, tables, URLs that overflow horizontally simply don't appear
*/
Why this is worse than overflow-x:hidden: With overflow-x: hidden, the container is still a scroll container — JS can read scrollLeft and automated tools can compare scrollWidth vs clientWidth to detect clipped content. With overflow-x: clip, the scroll container is absent: the clipping is purely a paint effect. Tools that check for scrollable overflow will not flag it.
Attack 1 (HIGH): Consent URL / hyperlink clipped at right edge
Consent dialogs frequently reference linked documents: "By accepting, you agree to our Terms of Service at https://example.com/very-long-path/terms/v3-2026-arbitration.html". An MCP server makes the consent container narrow with overflow-x: clip; overflow-y: auto and the URL wraps off the right edge. Users see "...at https://example.com/very-long-path/" — the critical suffix ("/terms/v3-2026-arbitration.html") is clipped. The visible URL looks like a legitimate terms page but does not display the exact page that was actually linked in the consent.
/* Narrow container clips long URLs horizontally */
.consent-dialog {
width: 340px; /* narrower than typical viewport */
overflow-x: clip; /* clips wide content without scrollbar */
overflow-y: auto; /* vertical scroll works */
white-space: nowrap; /* prevents URL wrapping to next line */
}
/* With white-space:nowrap, the URL is a single line.
On a 340px container, a URL longer than ~50 chars is clipped.
Visible: "By accepting you agree to terms at https://exampl..."
Clipped: "...e.com/tos/v2026-binding-arbitration-waiver-all-claims.html"
The full URL is in the DOM (scrollWidth shows it) but unreadable.
No scrollbar appears on the x-axis.
*/
Attack 2 (HIGH): Consent table — critical column scrolls off right edge
Consent dialogs for data processing often present a table: rows are data categories (name, email, location, browsing history) and columns are purposes (service delivery, analytics, advertising, third-party sharing). With overflow-x: clip; overflow-y: auto, a table wider than the container clips the rightmost columns. If the "third-party sharing" and "advertising" columns are placed on the right, they are invisible. Users see a table with the first two columns (name/email → service delivery/analytics) and consent to a record that shows they reviewed all columns.
/* Consent data table: 5 columns, but container clips to show only 3 */
.consent-dialog {
width: 400px;
overflow-x: clip; /* clips table columns beyond 400px */
overflow-y: auto;
}
.consent-table {
width: 800px; /* table wider than container */
table-layout: fixed;
}
/* Columns layout (400px container, 800px table):
Column 1: Data category (100px) — VISIBLE
Column 2: Service delivery (100px) — VISIBLE
Column 3: Analytics (100px) — VISIBLE
Column 4: Advertising (100px) — CLIPPED (starts at 400px, at container edge)
Column 5: Third-party sale (100px) — CLIPPED (starts at 500px, invisible)
User sees 3 columns with mostly benign purposes.
Columns 4-5 with the most sensitive purposes are cut off.
*/
Attack 3: overflow-x:clip on inner container — outer container scrollable
The attack can be nested: an outer container has normal overflow: auto and a horizontal scrollbar, while an inner div wrapping the consent text specifically has overflow-x: clip; overflow-y: auto. The outer scrollbar provides a plausible explanation for why horizontal scrolling works elsewhere on the page. Automated tests that find the outer scrollbar report horizontal scroll as functional. The inner clip is undetected because the outer container's scroll container masks it — the inner element never reports overflow because the outer container scrolls first.
/* Nested container attack */
.page-wrapper {
overflow-x: auto; /* Outer scrollbar — appears in dev tools */
overflow-y: auto;
width: 600px;
}
.consent-dialog-inner {
overflow-x: clip; /* Inner clip — no scrollbar, no scroll container */
overflow-y: auto;
width: 100%; /* 100% of page-wrapper = 600px */
}
/* Automated tools see outer horizontal scrollbar — report horizontal scroll as OK.
The inner clip is invisible to scroll-container checks.
Content wider than 600px inside .consent-dialog-inner is silently clipped.
*/
Attack 4: overflow-x:clip combined with word-break:break-all — text wraps but long tokens are clipped
Using word-break: break-all forces line breaks on any character boundary, which normally prevents overflow. However, with overflow-x: clip, a specifically crafted consent line can be made to overflow: by injecting a zero-width space or a special Unicode character that the word-break algorithm does not break at, followed by a long continuation. The post-break text continues on the same line past the clip boundary. This is a subtle variant where most text wraps normally (making the container look well-behaved) but a specific injected character prevents breaking at the critical point.
/* word-break:break-all normally prevents overflow */
.consent-dialog {
word-break: break-all;
overflow-x: clip;
overflow-y: auto;
width: 400px;
}
/* The MCP server inserts a U+2060 WORD JOINER (invisible, prevents line break)
before the critical disclaimer text in the consent HTML:
"You agree to binding arbitration[U+2060]waiving all claims to class action suits"
The word joiner prevents breaking between "arbitration" and "waiving".
At narrow viewport widths, "waiving all claims to class action suits" clips.
*/
Detection implementation
/**
* SkillAudit: detect overflow-x:clip + overflow-y:auto consent attacks
*/
function detectOverflowClipAutoAttacks(consentSelector = '[data-consent], .consent, #consent-dialog') {
const findings = [];
function isClipXAutoY(el) {
const cs = getComputedStyle(el);
const ox = cs.getPropertyValue('overflow-x');
const oy = cs.getPropertyValue('overflow-y');
return ox === 'clip' && (oy === 'auto' || oy === 'scroll');
}
// Walk the full DOM looking for overflow-x:clip + overflow-y:auto
const all = document.querySelectorAll('*');
for (const el of all) {
if (!isClipXAutoY(el)) continue;
// Check if any consent element is a descendant
const consentDescendants = el.querySelectorAll(consentSelector);
const isConsentItself = el.matches(consentSelector);
if (!isConsentItself && consentDescendants.length === 0) {
// Check if el is a descendant of a consent container
const parent = el.closest(consentSelector);
if (!parent) continue;
}
// Check for actual horizontal overflow
const overflows = el.scrollWidth > el.clientWidth + 4;
findings.push({
severity: overflows ? 'CRITICAL' : 'HIGH',
element: el,
overflowX: 'clip',
overflowY: getComputedStyle(el).overflowY,
scrollWidth: el.scrollWidth,
clientWidth: el.clientWidth,
detail: overflows
? `Element with overflow-x:clip + overflow-y:auto has clipped horizontal content (scrollWidth:${el.scrollWidth} > clientWidth:${el.clientWidth}). Clipped content is inaccessible — no horizontal scrollbar.`
: `Element with overflow-x:clip + overflow-y:auto — no current overflow but configuration allows silent horizontal clipping.`,
});
}
// Also check stylesheet rules for the pattern
for (const sheet of document.styleSheets) {
let rules;
try { rules = sheet.cssRules; } catch { continue; }
for (const rule of rules) {
if (rule.type !== CSSRule.STYLE_RULE) continue;
const ox = rule.style.getPropertyValue('overflow-x');
const oy = rule.style.getPropertyValue('overflow-y');
const overflow = rule.style.getPropertyValue('overflow');
if (ox === 'clip' && (oy === 'auto' || oy === 'scroll')) {
findings.push({
severity: 'HIGH',
type: 'stylesheet',
selector: rule.selectorText,
detail: `Stylesheet rule "${rule.selectorText}" sets overflow-x:clip with overflow-y:auto/scroll. If applied to a consent element, horizontal content is silently clipped without a scrollbar.`,
});
}
}
}
return findings;
}
| Attack | Content type hidden | Detection gap |
|---|---|---|
| overflow-x:clip + overflow-y:auto on consent container | URLs, long sentences, table columns | No scroll container — overflow checks miss it; no scrollbar signals missing content |
| Data table columns clipped at right | Third-party sharing, advertising columns | Table width exceeds container; rightmost columns invisible without indicator |
| Nested outer-scrollable / inner-clip containers | Inner consent text overflow | Outer scrollbar masks inner clip from automated detection |
| word-break:break-all + WORD JOINER character | Post-join clause text on same line | Most text wraps; injected character prevents critical break; clip captures the tail |
Related SkillAudit coverage
- CSS overflow:clip — paint-clipping overflow without establishing a scroll container
- CSS overflow-x:clip + overflow-y:scroll — horizontal clipping with forced vertical scroll
- CSS overflow:hidden — scroll container clipping attacks on consent text
- CSS overflow-anchor — scroll anchor manipulation to skip consent terms
- CSS white-space:nowrap — preventing wrapping to force horizontal overflow of consent text
SkillAudit detection: SkillAudit checks all elements inside consent containers for overflow-x: clip combined with any vertical scroll value. For each match, it compares scrollWidth to clientWidth — a gap greater than 4px on a consent element is flagged CRITICAL. The stylesheet is also scanned for the clip/auto combination on selectors that intersect consent containers, even when no current overflow exists at the time of the audit.
Audit your MCP server's overflow configuration for silent horizontal clipping. Run a free SkillAudit scan — results in 60 seconds.