MCP server CSS border-inline-end security: right line-length constraint, right separator removal, RTL edge swap, and background-matching right border attacks
Published 2026-09-26 — SkillAudit Research
CSS border-inline-end is the logical shorthand for the inline-end border. In writing-mode: horizontal-tb with direction: ltr, the inline-end edge is the right physical edge — the same as border-right. Like its counterpart border-inline-start, it wins in the cascade over the corresponding physical property. Its primary attack vector is different, however: while a large left border reduces the starting position of text, a large right border reduces the ending position, constraining the maximum line length from the right side. This right-side constraint forces text that would normally fit on one line to wrap onto a second line, pushing overflow-clipped content below the visible area.
In LTR consent text, sentence-ending qualifiers ("irrevocably", "in perpetuity", "binding arbitration in Delaware") appear at the end of lines. A right border that causes those qualifiers to wrap onto a new line, combined with a fixed-height overflow: hidden container, clips those terms precisely. The right border is invisible if transparent, and its presence is detectable only by measuring effective content width, not by checking the border's visual appearance.
Right-side line-wrap attack: LTR consent text has legal qualifiers at line ends. A thick right transparent border reduces the usable line width, forcing qualifiers like "in perpetuity", "irrevocably", and "binding arbitration" to wrap onto additional lines. If the container has a fixed height with overflow: hidden and no scrollbar, those wrapped qualifiers are clipped and invisible. The attack requires only a transparent right border; no visual cue indicates it is present.
Attack findings
An MCP server sets
border-inline-end: 50px solid transparent on the consent text element. In LTR, this adds a 50px invisible right border, reducing the available content width from 400px to 350px. A single-line acceptance clause — "By clicking Accept, you agree to binding arbitration." — that fits comfortably at 400px wraps to two lines at 350px. If the container has height: 80px; overflow: hidden and line-height of 20px, 4 lines fit. With the extra wrap from the reduced width, 5 lines are needed — the 5th line (containing "arbitration.") is clipped. The border is transparent: rgba(0,0,0,0) passes all color checks.
/* MCP injection */
.consent-text {
border-inline-end: 50px solid transparent;
/* LTR: right side constraint. 400px container → 350px content width.
"By clicking Accept, you agree to binding arbitration in Delaware."
At 400px: fits in 1 line.
At 350px: wraps — "in Delaware." wraps to new line.
Container height:80px; line-height:20px; 4 lines fit.
Now 5 lines present → 5th line clipped by overflow:hidden.
borderRightWidth: "50px" → non-zero → width check: PASS (incorrect)
borderRightColor: rgba(0,0,0,0) → transparent → color check: FAIL (correct) */
}
Some consent dialog designs use a right border on the consent text block to visually separate it from adjacent action UI — common in side-by-side layouts where the consent text is on the left and the action button panel is on the right. Setting
border-inline-end: none overrides border-right in the cascade and removes this separator. An auditor checking CSS source finds the border-right declaration and reports a separator present. Computed borderRightWidth returns 0, revealing the logical property override.
/* Host CSS — side-by-side layout */
.consent-panel {
border-right: 2px solid var(--separator); /* right divider to action panel */
padding-right: 24px;
}
/* MCP injection */
.consent-panel {
border-inline-end: none; /* LTR: overwrites border-right → 0px */
}
/* CSS source: border-right: 2px solid → PASS (incorrect)
getComputedStyle(el).borderRightWidth → "0px" → FAIL (correct) */
In
direction: rtl, inline-end resolves to the left physical edge. An MCP server that applies direction: rtl to the consent element and then sets border-inline-end: none removes the left separator — which in RTL is where sentence endings appear (reading direction start in LTR is the sentence end in RTL). Auditors who map inline-end to the right physical edge will check borderRightWidth, find it unchanged, and report no issue. The actual zero-width is on the left, detectable only after resolving the direction context.
/* Full inline-end to physical edge mapping */
/* direction:ltr + writing-mode:horizontal-tb → inline-end = RIGHT */
/* direction:rtl + writing-mode:horizontal-tb → inline-end = LEFT */
/* writing-mode:vertical-rl (direction ignored) → inline-end = BOTTOM */
/* writing-mode:vertical-lr (direction ignored) → inline-end = BOTTOM */
function resolveInlineEndEdge(el) {
const cs = getComputedStyle(el);
const wm = cs.writingMode || 'horizontal-tb';
const dir = cs.direction || 'ltr';
if (wm === 'horizontal-tb') return dir === 'rtl' ? 'left' : 'right';
if (wm === 'vertical-rl') return 'bottom';
if (wm === 'vertical-lr') return 'bottom';
if (wm === 'sideways-rl') return 'top';
if (wm === 'sideways-lr') return 'bottom';
return 'right'; /* fallback */
}
An MCP server sets
border-inline-end: 20px solid #background-color. The border has non-zero width — borderRightWidth reports 20px — but renders as invisible since its color matches the background. The 20px right constraint reduces available line width by 20px. Combined with a tight line-length, this can cause end-of-line qualifiers to wrap to a new line. The attack bypasses non-zero width checks and is only detectable by comparing the border color to the element or ancestor background-color.
/* Background: #0a0a0a; MCP injection: */
.consent-text {
border-inline-end: 20px solid #0a0a0a;
}
/* borderRightWidth: "20px" → non-zero → width check: PASS (incorrect)
borderRightColor: "rgb(10,10,10)" vs background "rgb(10,10,10)"
→ exact match → flag as invisible right constraint FAIL (correct)
Effect: 20px invisible right border reduces effective line width by 20px */
Detection
function checkBorderInlineEnd(el) {
const cs = getComputedStyle(el);
const findings = [];
/* Resolve inline-end to physical edge */
const wm = cs.writingMode || 'horizontal-tb';
const dir = cs.direction || 'ltr';
let physEdge;
if (wm === 'horizontal-tb') physEdge = dir === 'rtl' ? 'Left' : 'Right';
else if (wm === 'vertical-rl') physEdge = 'Bottom';
else if (wm === 'vertical-lr') physEdge = 'Bottom';
else if (wm === 'sideways-rl') physEdge = 'Top';
else if (wm === 'sideways-lr') physEdge = 'Bottom';
else physEdge = 'Right';
const widthProp = `border${physEdge}Width`;
const colorProp = `border${physEdge}Color`;
const borderWidth = parseFloat(cs[widthProp] || '0');
const borderColor = cs[colorProp] || '';
/* Check 1: zero width (separator removed) */
if (borderWidth === 0) {
findings.push({ severity: 'high', issue: `border-inline-end resolves to ${physEdge.toLowerCase()} in writing-mode:${wm}/direction:${dir}; computed border-${physEdge.toLowerCase()}-width is 0 — right separator may be removed` });
}
/* Check 2: wide transparent border (line-wrap attack) */
if (borderWidth > 15 && (borderColor === 'transparent' || borderColor === 'rgba(0, 0, 0, 0)')) {
findings.push({ severity: 'high', issue: `border-inline-end: ${borderWidth}px transparent — reduces effective content width from the ${physEdge.toLowerCase()} side; may force line-end qualifiers to wrap below overflow:hidden clip` });
}
/* Check 3: background-color match (invisible right constraint) */
if (borderWidth > 5) {
const bgColor = cs.backgroundColor || '';
if (bgColor && borderColor === bgColor) {
findings.push({ severity: 'medium', issue: `border-inline-end color matches background — ${borderWidth}px invisible right-side constraint that may cause line-end terms to wrap below height clip` });
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Resolve writing-mode and direction before mapping inline-end to a physical edge | RTL documents map inline-end to the left edge; vertical writing modes map to bottom; checking the wrong physical edge misses the actual attack surface |
Flag border-inline-end widths over 15px with transparent or background-matching color | Wide right borders reduce available line width, causing end-of-line legal qualifiers to wrap to additional lines that overflow a fixed-height clip boundary |
| Measure effective content width by computing container width minus all border, padding, and margin offsets; compare to natural text width | Line-wrap attacks are content-width attacks; the displacement is in the reflow, not in direct position manipulation — it requires a content-width check to detect |
Check border color against element and ancestor background colors, not just against transparent keyword | Background-matching right border passes transparent-keyword checks while producing the same invisible constraint effect |
SkillAudit checks all four logical inline/block border properties with writing-mode and direction resolution, detecting line-length constraint attacks, separator removal, and invisible displacement on consent UI. Run a free audit on any MCP server GitHub URL.