MCP server CSS overflow-clip-margin-block-end security: acceptance clause bottom-clipping, final permission removal, and vertical writing mode attacks
Published 2026-09-25 — SkillAudit Research
The CSS overflow-clip-margin-block-end property controls the clip extension on the block-end edge only when overflow: clip is applied. In standard horizontal writing (writing-mode: horizontal-tb), the block-end direction is downward — the physical bottom of the element. Its counterpart overflow-clip-margin-block-start targets the top, while this property targets the bottom. Together with the inline-start and inline-end longhands, these four properties form the complete per-edge overflow clip margin system.
The block-end attack is arguably the most legally dangerous of the four per-edge attacks. Consent dialogs are structured so that the legally binding conclusion — "By clicking Accept, you agree to the above terms", the arbitration clause, the data-sharing acknowledgment — appears at the end of the dialog, at the block-end. Clipping the block-end while leaving the authorization framing and permission list fully visible creates a consent dialog that looks complete but is missing its binding conclusion.
Why block-end is the highest-risk per-edge clip: The acceptance clause at the end of a consent dialog is not redundant with the permission list above it. The permission list tells users what the MCP server wants; the acceptance clause is the legal act of consenting. Removing it means the user sees a list of permissions and a button but never reads the clause that establishes that clicking the button constitutes binding agreement. Compare with block-start clipping, which removes the framing sentence at the top — here, we remove the legal conclusion at the bottom. See also: the CSS break-inside column trap for a related fragmentation-based attack pattern.
Attack findings
Setting
overflow-clip-margin-block-end: 0 with overflow: clip clips any content that overflows below the element's padding box. The attacker sizes the consent container so that it clips precisely at the bottom of the permission list, before the acceptance clause. The permission list remains visible; the "By clicking Accept, you agree to the above" line and any data-sharing acknowledgment that follows are in the DOM but clipped. Static auditors see a correctly-structured consent dialog with valid DOM content. The scrollHeight of the element is larger than clientHeight, but no scrollbar is shown (overflow:clip suppresses scrollbars).
.consent-dialog {
overflow: clip;
height: 200px; /* sized to show permission list, not acceptance clause */
/* Three edges get generous clip extension */
overflow-clip-margin-block-start: 20px;
overflow-clip-margin-inline-start: 20px;
overflow-clip-margin-inline-end: 20px;
/* Block-end is zero: bottom clips hard at padding edge */
overflow-clip-margin-block-end: 0;
}
/* scrollHeight > clientHeight but no scrollbar visible.
Acceptance clause at bottom: in DOM, not rendered. */
In
writing-mode: vertical-rl, the block-start is the physical left and the block-end is the physical right. Setting overflow-clip-margin-block-end: 0 in vertical-rl clips the right side of the element. For vertically typeset consent content, the final text block appears on the right — typically the acceptance clause and the binding acknowledgment. A static auditor that interprets block-end as "bottom edge" without resolving writing-mode will misidentify the targeted physical edge and underestimate the impact of the clip.
.consent-dialog {
writing-mode: vertical-rl;
overflow: clip;
overflow-clip-margin-block-start: 20px; /* clips LEFT edge — generous */
overflow-clip-margin-block-end: 0; /* clips RIGHT edge — acceptance clause */
}
/* In vertical-rl:
block-start = LEFT (first text block)
block-end = RIGHT (last text block = acceptance clause)
Zero block-end clips the rightmost column — the consent conclusion. */
Setting a visually prominent
overflow-clip-margin-block-start of 40px or more while setting overflow-clip-margin-block-end: 0 creates an asymmetric pair that is designed to fool auditors checking only whether "the element has some overflow-clip-margin." The generous start margin ensures the element passes any check of the form "is there a non-zero clip margin?" The zero end margin ensures the acceptance clause is clipped. An auditor must check each edge independently to catch this pattern.
An attacker can use JavaScript to set the container height dynamically based on viewport dimensions, always targeting a value slightly smaller than what would be needed to show the acceptance clause. On narrow screens (e.g. mobile), the permission list wraps to more lines and already fills the available height; the acceptance clause is pushed below the zero-margin block-end clip boundary. On wider screens, the same technique uses explicit
max-height with overflow: clip. The dynamic nature makes static analysis less reliable — the clip boundary only emerges at runtime given a specific viewport.
/* Dynamic height attack */
const dialog = document.querySelector('.consent-dialog');
const lineCount = dialog.querySelectorAll('.permission-item').length;
dialog.style.height = `${lineCount * 32 + 20}px`; /* exact height = items only */
/* overflow:clip on the element means block-end at this height clips
the acceptance clause that follows the last permission item */
Detection
function checkOverflowClipMarginBlockEnd(el) {
const cs = getComputedStyle(el);
if (cs.overflow !== 'clip' && cs.overflowX !== 'clip' && cs.overflowY !== 'clip') {
return null;
}
const marginBE = parseFloat(cs.getPropertyValue('overflow-clip-margin-block-end') || '0');
const marginBS = parseFloat(cs.getPropertyValue('overflow-clip-margin-block-start') || '0');
const marginIS = parseFloat(cs.getPropertyValue('overflow-clip-margin-inline-start')|| '0');
const marginIE = parseFloat(cs.getPropertyValue('overflow-clip-margin-inline-end') || '0');
const findings = [];
if (marginBE === 0 && (marginBS > 0 || marginIS > 0 || marginIE > 0)) {
const wm = cs.writingMode || 'horizontal-tb';
let physicalEdge = 'bottom';
if (wm === 'vertical-rl') physicalEdge = 'right';
if (wm === 'vertical-lr') physicalEdge = 'right';
if (wm === 'sideways-rl') physicalEdge = 'right';
if (wm === 'sideways-lr') physicalEdge = 'left';
findings.push({
severity: 'high',
issue: `overflow-clip-margin-block-end:0 while other edges have margins — asymmetric ${physicalEdge} edge clip removes acceptance clause at block-end`
});
}
// Flag if scrollHeight > clientHeight (content is clipped)
if (marginBE === 0 && el.scrollHeight > el.clientHeight + 4) {
findings.push({
severity: 'high',
issue: `overflow:clip with block-end:0 and scrollHeight (${el.scrollHeight}px) > clientHeight (${el.clientHeight}px) — acceptance clause content is clipped`
});
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
| Audit all four overflow-clip-margin longhands independently | The asymmetric zero-end pattern is only visible when longhands are checked individually, not via shorthand inspection |
| Compare scrollHeight to clientHeight on overflow:clip elements | When scrollHeight exceeds clientHeight and overflow:clip is set with zero block-end margin, content at the bottom is guaranteed to be clipped |
| Resolve writing-mode before interpreting block-end as a physical edge | Block-end is bottom in horizontal-tb but right in vertical-rl — resolving prevents misidentified attack targets |
| Verify the acceptance clause bounding rect is within the clip region | Query the last child text node of the consent dialog and confirm its bottom edge is inside the element's computed clip boundary |
SkillAudit checks all four overflow-clip-margin longhands with writing-mode-aware physical edge resolution, and flags zero block-end margins that clip the acceptance clause at the bottom of consent dialogs. Run a free audit on any MCP server GitHub URL.