MCP server CSS border-inline-start security: left separator removal, transparent left displacement, RTL edge misdirection, and invisible left padding attacks
Published 2026-09-26 — SkillAudit Research
CSS border-inline-start is the logical shorthand for the inline-start border. In writing-mode: horizontal-tb with direction: ltr (LTR), the inline-start edge is the left physical edge — the same as border-left. Like all logical properties, it wins in the cascade over its corresponding physical counterpart: border-left: 3px solid #accent; border-inline-start: none results in no left border. An auditor reading CSS source finds the border-left declaration; computed styles reveal the zero-width override.
Consent dialogs styled with a left accent border — a common pattern for highlighted information blocks and legal notice areas — depend on that border to signal importance and formality. MCP servers can exploit border-inline-start to remove the left accent, introduce an invisible thick left border that constrains the content area, or exploit direction: rtl to redirect which physical edge is targeted.
LTR vs RTL inline-start: In LTR documents, inline-start is the left edge. In RTL documents, inline-start is the right edge. An MCP server that sets direction: rtl on the consent element or its ancestor and then sets border-inline-start: 50px solid transparent is actually adding a thick transparent right border in LTR terms — but the logical-property name still reads as "left" to direction-unaware auditors checking CSS source for left-side manipulation.
Attack findings
The host renders a left accent border on the consent text block using
border-left: 3px solid var(--accent). This visual cue frames the consent content as a formal notice. The MCP server injects border-inline-start: none, which in LTR overwrites border-left in the cascade, removing the left accent. The consent block loses its visual framing. CSS source audits find the border-left declaration and report the accent present. Computed borderLeftWidth returns 0, revealing the override.
/* Host CSS */
.consent-notice {
border-left: 3px solid var(--accent); /* left accent — signals importance */
padding-left: 16px;
}
/* MCP injection */
.consent-notice {
border-inline-start: none;
/* In LTR horizontal-tb:
border-inline-start wins cascade over border-left.
Computed borderLeftWidth → "0px" despite source declaring "3px solid" */
}
/* CSS source: border-left: 3px solid → PASS (incorrect)
getComputedStyle(el).borderLeftWidth → "0px" → FAIL (correct) */
A thick transparent left border (
border-inline-start: 50px solid transparent) acts as invisible left padding, reducing the available content width. In a 400px wide consent container, a 50px left border leaves only 350px for text. If the acceptance clause — "By clicking Accept, you agree to binding arbitration in Delaware" — fits in 400px on one line but requires two lines at 350px, the additional wrap pushes it below a fixed-height overflow container's visible boundary. The border is transparent, so color checks pass. The effect is only detectable by measuring the effective content width and comparing it to the expected layout.
/* MCP injection */
.consent-text {
border-inline-start: 50px solid transparent;
/* LTR: adds 50px invisible left border.
Content area width shrinks from 400px to 350px.
Acceptance clause wraps to extra line.
Container: height:80px; overflow:hidden; 2 lines at 20px each = 40px text.
With wrap: 3rd line pushes below 80px clip height.
"you agree to binding arbitration in Delaware" → clipped.
transparent → rgba(0,0,0,0) → color checks: PASS (incorrect)
borderLeftWidth: "50px" → non-zero → width check: PASS (incorrect)
BUT displacement from content-width reduction is the real attack */
}
In
direction: rtl, inline-start resolves to the right physical edge. An MCP server that applies direction: rtl to the consent element and then sets border-inline-start: none removes the right border — which in RTL is where sentence openings appear (the reading direction start). An auditor checking borderLeftWidth finds the value unchanged and reports no issue. The actual removed separator is on the right side, revealed only by checking the writing-mode and direction context before mapping the logical property to a physical edge.
/* Full inline-start to physical edge mapping */
/* direction:ltr + writing-mode:horizontal-tb → inline-start = LEFT */
/* direction:rtl + writing-mode:horizontal-tb → inline-start = RIGHT */
/* writing-mode:vertical-rl (direction ignored) → inline-start = TOP */
/* writing-mode:vertical-lr (direction ignored) → inline-start = TOP */
function resolveInlineStartEdge(el) {
const cs = getComputedStyle(el);
const wm = cs.writingMode || 'horizontal-tb';
const dir = cs.direction || 'ltr';
if (wm === 'horizontal-tb') return dir === 'rtl' ? 'right' : 'left';
if (wm === 'vertical-rl') return 'top';
if (wm === 'vertical-lr') return 'top';
if (wm === 'sideways-rl') return 'bottom';
if (wm === 'sideways-lr') return 'top';
return 'left'; /* fallback */
}
An MCP server sets
border-inline-start: 20px solid #background-color where the color matches the page background. The border has non-zero width, passing width checks, but renders as invisible — the same color as the background behind it. In LTR, this adds 20px of invisible left content offset, potentially pushing wrapped lines to a shorter effective width. Combined with a tight fixed-height container, this can clip the last line of the acceptance clause. The attack is detectable only by comparing the border color to the background color, not by checking border presence or width alone.
/* Background: #0a0a0a (dark theme); MCP injection: */
.consent-text {
border-inline-start: 20px solid #0a0a0a; /* matches background */
}
/* Checks:
borderLeftWidth: "20px" → non-zero → PASS (incorrect)
borderLeftStyle: "solid" → PASS (incorrect)
borderLeftColor: "rgb(10, 10, 10)" vs background "rgb(10, 10, 10)"
→ exact match → flag as background-matching border FAIL (correct) */
Detection
function checkBorderInlineStart(el) {
const cs = getComputedStyle(el);
const findings = [];
/* Resolve inline-start to physical edge */
const wm = cs.writingMode || 'horizontal-tb';
const dir = cs.direction || 'ltr';
let physEdge;
if (wm === 'horizontal-tb') physEdge = dir === 'rtl' ? 'Right' : 'Left';
else if (wm === 'vertical-rl') physEdge = 'Top';
else if (wm === 'vertical-lr') physEdge = 'Top';
else if (wm === 'sideways-rl') physEdge = 'Bottom';
else if (wm === 'sideways-lr') physEdge = 'Top';
else physEdge = 'Left';
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-start resolves to ${physEdge.toLowerCase()} in writing-mode:${wm}/direction:${dir}; computed border-${physEdge.toLowerCase()}-width is 0 — left accent separator may be removed` });
}
/* Check 2: wide transparent border (content width reduction) */
if (borderWidth > 15 && (borderColor === 'transparent' || borderColor === 'rgba(0, 0, 0, 0)')) {
findings.push({ severity: 'high', issue: `border-inline-start: ${borderWidth}px transparent — reduces effective content width; may force extra line wrapping that clips acceptance clause in overflow:hidden container` });
}
/* Check 3: background-matching color (invisible spacing) */
if (borderWidth > 5) {
const bgColor = cs.backgroundColor || '';
if (bgColor && borderColor === bgColor) {
findings.push({ severity: 'medium', issue: `border-inline-start color matches element background (${borderWidth}px) — invisible border spacing that may displace content` });
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Read computed borderLeftWidth (or direction-resolved physical edge) rather than CSS source border-left | Logical property overrides win the cascade without appearing to change the physical property declaration in source |
Resolve both writing-mode and direction before mapping inline-start to a physical edge | RTL direction flips inline-start from left to right; vertical writing modes map to top or bottom; checking the wrong physical edge misses the actual removed separator |
Flag border-inline-start widths over 15px with transparent color | Wide transparent left borders reduce effective content width, forcing line wrapping that can push acceptance clauses below overflow:hidden clip boundaries |
| Compare computed border color to element and ancestor background colors | Background-matching left borders render as invisible spacing without reducing computed width to zero, bypassing width-only checks |
SkillAudit audits all logical border properties — border-inline-start, border-inline-end, border-block-start, and border-block-end — with full writing-mode and direction resolution, detecting separator removal, transparent displacement, and camouflage attacks on consent UI. Run a free audit on any MCP server GitHub URL.