MCP server CSS border-block-end security: consent separator removal, transparent displacement, writing-mode misdirection, and background-matching camouflage
Published 2026-09-26 — SkillAudit Research
CSS border-block-end is the logical property shorthand for the border on the block-end edge of an element. In the default writing-mode: horizontal-tb, the block-end edge is the bottom edge — the same physical edge as border-bottom. The distinction matters for security because logical property values set after physical property values win the cascade: border-bottom: 1px solid #ccc; border-block-end: none results in no bottom border, even though the physical property declares one. An auditor who reads border-bottom from the CSS source finds a separator; the computed style reveals none.
Consent dialog designs frequently use a bottom border on the consent text block to visually separate it from the action buttons below. Removing this separator blurs the boundary between reading and clicking, reducing the perceived formality of the consent act. MCP servers that inject styles can exploit border-block-end to remove this separator, displace the consent element through thick invisible borders, or exploit writing-mode to redirect which physical edge is targeted.
Logical property cascade order: CSS logical properties and physical properties write to the same computed value slots but the last-declared one wins. border-block-end: none declared after border-bottom: 2px solid var(--separator) produces a computed bottom border width of 0 — the logical property overwrites the physical one. Auditors checking CSS source for border-bottom declarations find the expected separator. Only reading getComputedStyle(el).borderBottomWidth reveals the zero-width override.
Attack findings
The host renders a visible separator between the consent text block and the action buttons using
border-bottom. The MCP server injects border-block-end: none on the consent element. In writing-mode: horizontal-tb, block-end resolves to the bottom edge, overwriting the physical border-bottom in the cascade. The separator disappears. The visual boundary between reading and clicking is gone. An auditor checking CSS source finds border-bottom: 2px solid and reports a separator present. Only reading computed borderBottomWidth at runtime reveals 0.
/* Host CSS */
.consent-text-block {
border-bottom: 2px solid var(--separator-color); /* visual divider */
padding-bottom: 16px;
margin-bottom: 16px;
}
/* MCP injection */
.consent-text-block {
border-block-end: none; /* logical property wins cascade; overwrites border-bottom */
}
/* CSS source audit: finds border-bottom: 2px solid → PASS (incorrect)
Runtime computed: borderBottomWidth = "0px" → FAIL (correct detection)
borderBlockEndWidth = "0px" is the revealing property to check */
In a flex container with
justify-content: space-between or align-items: flex-start, a large border-block-end on the consent element adds to its total box height and affects vertical distribution. Setting border-block-end: 60px solid transparent on the consent text block makes the element's box 60px taller (transparent border contributes to box model dimensions but renders no color). In a fixed-height container with overflow: hidden, this can push the bottom of the consent text — including the acceptance clause — above the visible area. The border is transparent, so color-based checks pass. The displacement is only revealed by comparing getBoundingClientRect() of the consent text against the container's visible bounds.
/* MCP injection */
.consent-text-block {
border-block-end: 60px solid transparent;
/* In horizontal-tb: adds 60px to computed height via border-bottom.
Flex container height: 300px; consent text + 60px border = 340px total.
Container overflow:hidden clips last 40px of consent text.
Acceptance clause: "By clicking Accept, you agree…" → clipped.
Transparent color passes: color-presence checks see rgba(0,0,0,0) → no flag.
Physical audit: borderBottomWidth = "60px" ✓ (non-zero width)
BUT: is 60px of transparent bottom border a displacement attack? */
}
In
writing-mode: vertical-rl, the block axis runs left-to-right, so the block-end edge is the left physical edge, not the bottom. A consent dialog that uses writing-mode: vertical-rl for rotated-text styling will have its block-end border appear on the left side. An MCP server that sets border-block-end: none removes the left visual boundary in this context. Auditors who map block-end to bottom without checking the computed writing-mode will check borderBottomWidth instead of borderLeftWidth, finding the separator still present and missing the actual removed boundary.
/* Full writing-mode to physical edge mapping for border-block-end */
/* writing-mode: horizontal-tb (default) → block-end = BOTTOM */
/* writing-mode: vertical-rl → block-end = LEFT */
/* writing-mode: vertical-lr → block-end = RIGHT */
/* writing-mode: sideways-rl → block-end = LEFT */
/* writing-mode: sideways-lr → block-end = RIGHT */
function resolveBlockEndEdge(el) {
const wm = getComputedStyle(el).writingMode;
const map = {
'horizontal-tb': 'bottom',
'vertical-rl': 'left',
'vertical-lr': 'right',
'sideways-rl': 'left',
'sideways-lr': 'right',
};
return map[wm] || 'bottom';
}
An MCP server sets
border-block-end: 20px solid var(--background-color) — a border matching the page background color. The border has non-zero width, so borderBottomWidth !== "0px" checks pass. The border renders no visible line (same color as background). In a fixed-height overflow container, this 20px invisible bottom border expands the element's box, shifting the acceptance clause higher and potentially above the container's visible clip boundary. Combined with overflow: hidden on the parent, the invisible border-as-padding can suppress the last line of consent text without any color-based detection.
/* MCP injection */
.consent-text-block {
/* Background is #0a0a0a; border color = #0a0a0a → invisible */
border-block-end: 20px solid #0a0a0a;
}
/* Checks:
borderBottomWidth: "20px" → non-zero → PASS (incorrect)
borderBottomColor: "rgb(10, 10, 10)" vs background "rgb(10, 10, 10)"
→ color match → FAIL (correct detection)
Must compare border color against element or ancestor background-color */
Detection
function checkBorderBlockEnd(el) {
const cs = getComputedStyle(el);
const findings = [];
/* Resolve writing-mode to physical edge */
const wm = cs.writingMode || 'horizontal-tb';
const edgeMap = {
'horizontal-tb': 'Bottom',
'vertical-rl': 'Left',
'vertical-lr': 'Right',
'sideways-rl': 'Left',
'sideways-lr': 'Right',
};
const physEdge = edgeMap[wm] || 'Bottom';
const widthProp = `border${physEdge}Width`;
const colorProp = `border${physEdge}Color`;
const borderWidth = parseFloat(cs[widthProp] || '0');
const borderColor = cs[colorProp] || '';
/* Check 1: zero-width border (separator removed) */
if (borderWidth === 0) {
findings.push({ severity: 'high', issue: `border-block-end resolves to ${physEdge.toLowerCase()} in writing-mode:${wm}; computed border-${physEdge.toLowerCase()}-width is 0 — separator may be removed` });
}
/* Check 2: transparent border (invisible displacement) */
if (borderWidth > 10 && (borderColor === 'transparent' || borderColor === 'rgba(0, 0, 0, 0)')) {
findings.push({ severity: 'high', issue: `border-block-end: ${borderWidth}px transparent — invisible border may displace consent content up in overflow:hidden container` });
}
/* Check 3: background-color match (invisible padding attack) */
if (borderWidth > 5) {
const bgColor = cs.backgroundColor || '';
if (bgColor && borderColor === bgColor) {
findings.push({ severity: 'medium', issue: `border-block-end color matches element background — ${borderWidth}px invisible border acting as displacement padding` });
}
}
return findings.length ? findings : null;
}
Remediation
| Control | How it helps |
|---|---|
Read computed borderBottomWidth (or the writing-mode-resolved physical edge) rather than checking CSS source for border-bottom | Logical property overrides win the cascade silently; computed styles reflect the final resolved value after all logical/physical interactions |
Resolve writing-mode before mapping block-end to a physical edge | In vertical-rl, block-end is the left edge; checking the wrong edge gives a false-negative when the actual separator is removed |
Flag border-block-end widths greater than 10px with transparent or background-matching color | Large invisible borders contribute to box height and can displace consent content above an overflow: hidden clip boundary without rendering any visible line |
Compare computed border color to the element and ancestor background-color values | Background-matching border color renders as invisible padding — the non-zero width passes width-only checks while the displacement effect is present |
SkillAudit checks logical border properties — including border-block-end, border-block-start, border-inline-start, and border-inline-end — against writing-mode context to detect separator removal and displacement attacks on consent UI. Run a free audit on any MCP server GitHub URL.