MCP server CSS overflow-x security: nowrap horizontal text clip, text-indent 500px off-screen start, negative margin-left left-side clip, and JS mousedown overflow-x injection
Published 2026-08-07 — SkillAudit Research
The CSS overflow-x property controls whether content extending beyond the left or right edges of a block container is clipped, scrolled, or visible. Setting overflow-x: hidden clips any content that extends past the container's horizontal boundaries without creating a scrollbar. While general overflow: hidden (shorthand) clips in both axes, the axis-specific overflow-x: hidden is sometimes checked separately — and scanners that verify overflow: hidden via the shorthand property may not flag overflow-x: hidden set independently. This creates an evasion surface for MCP consent bypass attacks that rely on horizontal clipping.
Combined with white-space: nowrap, text-indent, or negative horizontal margins, overflow-x: hidden can push entire consent texts off the visible right or left edge of the container while leaving the consent element in the DOM with valid computed dimensions, display: block, visibility: visible, and non-zero offsetWidth/offsetHeight. The element's scrollWidth exceeds offsetWidth, revealing the horizontal overflow — but only if the scanner checks both values. See also CSS overflow attacks, CSS text-overflow attacks, and CSS white-space attacks.
overflow-x vs overflow shorthand — scanner evasion: overflow: hidden is a shorthand that sets both overflow-x and overflow-y to hidden. But overflow-x: hidden can be set independently without setting overflow-y. A scanner that checks getComputedStyle(el).overflow === 'hidden' will get 'visible' when only overflow-x is set — because the shorthand reads as the value for both axes combined, which is not hidden when only one axis is. The correct check is overflowX and overflowY separately: getComputedStyle(el).overflowX and getComputedStyle(el).overflowY.
Attack 1: overflow-x:hidden + white-space:nowrap — consent extends off right edge, clipped (SA-CSS-OVFX-001)
The consent element sets white-space: nowrap and its parent sets overflow-x: hidden with a fixed width of 200px. With white-space: nowrap, the consent text does not wrap to a second line — it extends as a single long horizontal line that may be 600px or more in total width. The parent clips everything past the 200px right edge. Only the first ~30 characters of the consent are visible. The install button, shown in a separate element outside the overflow-x container, is fully visible. The consent element's offsetHeight is normal (single line height); its scrollWidth is 600px but offsetWidth is 200px. The scanner must compare scrollWidth > offsetWidth on the container to detect the clip.
/* MCP attack: */
.consent-container {
width: 200px;
overflow-x: hidden; /* only horizontal axis clipped — overflow shorthand returns 'visible' */
/* overflow: visible (shorthand) ← scanner checking .overflow gets 'visible'; evasion succeeds */
/* overflowX: 'hidden' ← correct check */
}
.consent-disclosure {
white-space: nowrap; /* forces single horizontal line */
/* scrollWidth: ~600px (full consent text)
offsetWidth: 200px (parent width)
Visible text: "By clicking Install, you" (truncated — 200px worth)
Hidden: "... agree to grant this MCP server access to your file system." */
}
// Detection:
function detectNowrapHorizontalClip(consentEl) {
const cs = window.getComputedStyle(consentEl);
const parent = consentEl.parentElement;
if (!parent) return;
const parentCS = window.getComputedStyle(parent);
const overflowX = parentCS.overflowX; // NOT parentCS.overflow
if (['hidden', 'clip'].includes(overflowX) && cs.whiteSpace === 'nowrap') {
const scrollWidth = consentEl.scrollWidth;
const offsetWidth = consentEl.offsetWidth;
if (scrollWidth > offsetWidth * 1.2) {
console.error('SA-CSS-OVFX-001: overflow-x:hidden + white-space:nowrap clips consent text', {
consentEl, overflowX,
scrollWidth, offsetWidth,
hiddenCharRatio: (1 - offsetWidth / scrollWidth).toFixed(2)
});
}
}
}
Attack 2: overflow-x:hidden + text-indent:500px — consent text starts off right edge (SA-CSS-OVFX-002)
The consent element sets text-indent: 500px combined with the parent's overflow-x: hidden. The CSS text-indent property indents the first line of text by the specified amount — in this case, 500px from the container's left edge. On a 200px wide container with overflow-x: hidden, the first character of the consent text begins 500px from the left: 300px to the right of the container's right boundary. The entire consent text is off-screen. The consent element has a valid height (determined by content, which the browser still lays out), a valid layout width, and display: block. The element appears as an empty white box. The scanner must check text-indent relative to the container's clientWidth to detect this.
/* MCP attack: */
.consent-container { width: 200px; overflow-x: hidden; }
.consent-disclosure {
text-indent: 500px; /* first (and only) line starts 500px from left */
white-space: nowrap;
/* Visible region: 0–200px from left = empty (500px indent starts at 500px)
All consent text is at 500px + onwards — entirely off-screen
Element has offsetHeight > 0 (layout computed); looks like empty white div */
}
/* Percentage variant — adapts to container width: */
.consent-disclosure-pct {
text-indent: 150%; /* 150% of container width = 300px on 200px container */
white-space: nowrap;
/* Scanner checking fixed px thresholds misses % indents */
}
// Detection:
function detectTextIndentClip(consentEl) {
const cs = window.getComputedStyle(consentEl);
const parent = consentEl.parentElement;
if (!parent) return;
const parentCS = window.getComputedStyle(parent);
if (!['hidden', 'clip'].includes(parentCS.overflowX)) return;
// getComputedStyle returns resolved px value even for % indents
const textIndentPx = parseFloat(cs.textIndent) || 0;
const containerWidth = parent.clientWidth;
if (textIndentPx > containerWidth) {
console.error('SA-CSS-OVFX-002: text-indent:' + textIndentPx + 'px exceeds container width ' + containerWidth + 'px — consent text starts off-screen', {
consentEl, textIndentPx, containerWidth,
overflow: parentCS.overflowX
});
}
}
Attack 3: overflow-x:hidden + negative margin-left — left portion of consent clipped (SA-CSS-OVFX-003)
The consent element sets margin-left: -300px; width: 500px inside a parent with overflow-x: hidden; width: 200px. The consent element's content begins 300px to the left of the parent's left edge — outside the visible clipping area. The visible portion of the consent element starts 300px into the text (mid-sentence). From the user's perspective, the consent text begins with a fragment like "...install and grant full file-system access." — with the beginning ("I agree to...") invisible. The consent element has valid layout dimensions and exists in the DOM. The attack exploits that the container clips negative-margin overflow on the left side specifically. The getBoundingClientRect().left of the consent element is negative (off-screen left), revealing the clip.
/* MCP attack: */
.consent-container {
width: 200px;
overflow-x: hidden; /* clips negative-margin content on left */
}
.consent-disclosure {
width: 500px;
margin-left: -300px; /* element starts 300px left of container (off-screen left) */
/* Visible window: 0–200px of the container
= pixels 300–500 of the consent element (mid-sentence)
User sees: "...to install and grant filesystem access."
Hidden: "By clicking Install, you agree..." */
}
// Detection:
function detectNegativeMarginHorizontalClip(consentEl) {
const cs = window.getComputedStyle(consentEl);
const marginLeft = parseFloat(cs.marginLeft) || 0;
if (marginLeft < 0) {
const parent = consentEl.parentElement;
if (!parent) return;
const parentCS = window.getComputedStyle(parent);
if (['hidden', 'clip'].includes(parentCS.overflowX)) {
const consentBCR = consentEl.getBoundingClientRect();
const parentBCR = parent.getBoundingClientRect();
// Is the left portion of consent clipped?
if (consentBCR.left < parentBCR.left) {
const clippedPx = parentBCR.left - consentBCR.left;
console.error('SA-CSS-OVFX-003: negative margin-left + overflow-x:hidden clips left ' + clippedPx.toFixed(0) + 'px of consent', {
consentEl, marginLeft, clippedPx, overflowX: parentCS.overflowX
});
}
}
}
}
Attack 4: JS mousedown sets overflow-x:hidden + white-space:nowrap — consent clipped at install click (SA-CSS-OVFX-004)
At page load, the consent is fully visible with normal text wrapping in a container with overflow-x: visible (default). When the user presses the install button, a mousedown listener fires and simultaneously sets the container's overflowX to 'hidden' and the consent element's whiteSpace to 'nowrap'. The consent text instantly collapses from a multi-line block to a single line that extends far past the now-clipped right edge. Only the first ~30 characters remain visible. The browser registers the click and confirms the install. MutationObserver on both the container and consent element detects the style change; comparing scrollWidth to offsetWidth immediately after the event confirms the text is being clipped.
// MCP JS — fires at mousedown:
document.querySelector('#install-btn').addEventListener('mousedown', () => {
const container = document.querySelector('.consent-container');
const consent = document.querySelector('.consent-disclosure');
if (container && consent) {
container.style.overflowX = 'hidden'; /* axis-specific — evades overflow shorthand check */
consent.style.whiteSpace = 'nowrap'; /* collapses to single line */
/* Consent text clips to ~30 chars immediately
User sees only beginning of consent — critical clause hidden */
}
}, { capture: true });
// Detection:
function detectDynamicOverflowXInjection(containerEl, consentEl) {
new MutationObserver(() => {
const containerCS = window.getComputedStyle(containerEl);
const consentCS = window.getComputedStyle(consentEl);
if (['hidden', 'clip'].includes(containerCS.overflowX) &&
consentCS.whiteSpace === 'nowrap') {
requestAnimationFrame(() => {
if (consentEl.scrollWidth > consentEl.offsetWidth * 1.2) {
console.error('SA-CSS-OVFX-004: JS injected overflow-x:hidden + white-space:nowrap at install click', {
containerEl, consentEl,
overflowX: containerCS.overflowX,
scrollWidth: consentEl.scrollWidth,
offsetWidth: consentEl.offsetWidth
});
}
});
}
}).observe(containerEl, { attributes: true, attributeFilter: ['style', 'class'] });
new MutationObserver(() => {
const consentCS = window.getComputedStyle(consentEl);
if (consentCS.whiteSpace === 'nowrap') {
const containerCS = window.getComputedStyle(containerEl);
if (['hidden', 'clip'].includes(containerCS.overflowX)) {
console.error('SA-CSS-OVFX-004: dynamic nowrap + overflow-x:hidden detected on consent', { consentEl });
}
}
}).observe(consentEl, { attributes: true, attributeFilter: ['style', 'class'] });
document.querySelector('#install-btn, [data-action="install"]')
?.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
}
Root detection method: Check getComputedStyle(parent).overflowX independently from getComputedStyle(parent).overflow. The shorthand overflow property does not reflect axis-specific overrides; overflowX and overflowY must be checked separately. Then verify: consentEl.scrollWidth > consentEl.offsetWidth (horizontal overflow exists); getComputedStyle(consentEl).textIndent compared to container clientWidth; and consentEl.getBoundingClientRect().left vs parent getBoundingClientRect().left. SkillAudit checks axis-specific overflow on all ancestor elements and correlates with text-indent, white-space, and horizontal margin values.
Attack summary
| ID | Technique | overflow shorthand | overflowX | scrollWidth check | Severity |
|---|---|---|---|---|---|
| SA-CSS-OVFX-001 | overflow-x:hidden + white-space:nowrap — right-edge clip | visible (evades) | hidden (reveals) | required | High |
| SA-CSS-OVFX-002 | text-indent:500px + overflow-x:hidden — consent starts off-screen | visible (evades) | hidden (reveals) | required | High |
| SA-CSS-OVFX-003 | margin-left:-300px + overflow-x:hidden — left portion clipped | visible (evades) | hidden (reveals) | required (negative BCR) | High |
| SA-CSS-OVFX-004 | JS mousedown sets overflowX:hidden + nowrap at install click | visible (evades) | hidden (reveals) | required (dynamic) | High |
Consolidated findings
See also: CSS overflow (shorthand) attacks | CSS text-overflow attacks | CSS white-space attacks | CSS negative margin attacks | SkillAudit — free MCP server audit