Security Guide
MCP server CSS border-inline security — border-inline-start: 50vw width collapse, RTL dir attribute side-swap, inline-axis width annihilation, JS mousedown injection
The CSS border-inline shorthand and its sub-properties border-inline-start and border-inline-end set borders along the logical inline axis of an element. In LTR horizontal text, border-inline-start is the physical left border and border-inline-end is the physical right border — but adding dir="rtl" to a consent container swaps these mappings, creating a gap between defenses that check physical border names and attacks that use logical property names. For MCP servers with CSS injection capability, this creates four attack surfaces targeting the inline-axis width of consent text containers.
CSS border-inline — property overview
The border-inline shorthand sets border-inline-width, border-inline-style, and border-inline-color for both the inline-start and inline-end edges. The sub-properties border-inline-start and border-inline-end target individual inline-axis edges. In LTR horizontal text (direction: ltr, writing-mode: horizontal-tb): border-inline-start = physical left, border-inline-end = physical right. In RTL (direction: rtl): border-inline-start = physical right, border-inline-end = physical left. In writing-mode: vertical-rl: the inline axis is vertical, so border-inline-start = physical top. The security implication: a physical-property security check misses logical-property attacks when the direction or writing mode differs from the assumed default.
Attack 1: border-inline-start: 50vw — collapsing consent text width
A border-inline-start width set to a viewport-relative percentage (e.g., 50vw) on a consent text container consumes half the viewport width as a border. If the container itself is 400px wide and the viewport is 900px wide, a 50vw border-inline-start = 450px — wider than the container, collapsing the content area to zero. Even at more modest values, a border-inline-start consuming most of the container's inline space pushes consent text into an extremely narrow column — a column too narrow to display full permission strings — which the browser handles by word-wrapping or clipping depending on the overflow and white-space settings. The border itself is invisible if colored to match the background.
/* Attack: viewport-relative border-inline-start collapses consent text width */
.consent-text, .permission-disclosure {
border-inline-start-width: 50vw !important; /* may exceed container width */
border-inline-start-style: solid !important;
border-inline-start-color: var(--dialog-bg, #fff) !important; /* invisible */
overflow: hidden !important; /* clip the zero-width content */
}
/* Effect for a 400px consent container on a 1200px viewport:
border-inline-start = 50vw = 600px (exceeds container width of 400px)
content inline size = max(0, 400 - 600) = 0px (browser clamps to 0)
overflow: hidden clips all content into 0px width
textContent: "Allow SkillAudit filesystem access" ← unchanged
getBoundingClientRect().width: 400px ← unchanged (border-box sizing)
visibility: 'visible', opacity: '1', display: 'block' ← all pass
Rendered: blank white box (border only, no content visible)
Conservative variant (less detectable — 60% collapse):
border-inline-start-width: calc(60% - 0px) (relative to container)
→ consent text forced into 40% of available width
→ long permission strings ("read, write, delete, execute all files")
overflow-wrap and clip to visible area
→ user sees only partial permission string ("read, write, delete,")
with "execute all files" clipped outside visible area */
/* Detection: compare border-inline-start-width to container clientWidth */
function checkInlineCollapse(el) {
const cs = getComputedStyle(el);
const bw = parseFloat(cs.getPropertyValue('border-inline-start-width')) || 0;
const cw = el.clientWidth;
if (bw > cw * 0.3) return { severity: 'HIGH', detail: `border-inline-start: ${bw}px on ${cw}px container` };
}
Attack 2: RTL dir attribute + border-inline-end — physical-side swap
An MCP server that can modify HTML attributes can add dir="rtl" to a consent container element. This swaps the inline-start/inline-end mapping: in an RTL container, border-inline-start becomes the physical right border and border-inline-end becomes the physical left border. A security defense checking getComputedStyle(el).borderLeft will not see a large value — the injected CSS uses border-inline-end (which in RTL is the physical left side) and the physical border-left property reads as zero. The attack injects through the logical property name while the defense inspects the physical property name.
/* Injection: add dir="rtl" to consent container, then inject border-inline-end */
/* Step 1: DOM mutation to add RTL direction */
document.querySelector('.consent-dialog').setAttribute('dir', 'rtl');
/* Step 2: CSS targeting inline-end (which in RTL = physical left) */
.consent-dialog[dir="rtl"] .consent-text {
border-inline-end-width: 80% !important; /* RTL: this is the PHYSICAL LEFT border */
border-inline-end-style: solid !important;
border-inline-end-color: #ffffff !important;
overflow: hidden !important;
}
/* Effect:
- Physical border-left: 80% of container width consumed as white border
- Consent text squeezed into 20% of container on the physical right side
- Physical property check: getComputedStyle(el).borderLeft = '0px none' ← MISS
(border-inline-end ≠ border-left in RTL context)
- Logical property check: getComputedStyle(el).borderInlineEndWidth = '320px' ← CATCH
Additional effect: RTL mode also reverses text reading direction —
Arabic and Hebrew consent text renders RTL naturally, but LTR English
rendered in an RTL container has its sentence order reversed at the
Unicode bidirectional algorithm level, potentially scrambling English
permission disclosures into unreadable sequences.
Detection: check dir attribute and border-inline-* properties together */
Physical vs. logical property check gap. getComputedStyle(el).borderLeft and getComputedStyle(el).getPropertyValue('border-inline-end-width') return different values in RTL containers. A scanner that only checks physical border property names will not detect logical border attacks in non-default direction contexts. The correct approach: always check both physical and logical property variants, or exclusively check logical variants which resolve correctly in all direction contexts.
Attack 3: combined border-inline-start + border-inline-end — total width annihilation
When both border-inline-start and border-inline-end are set, their combined widths are subtracted from the container's total width to produce the content-box inline size. If their sum meets or exceeds the container width, the content area is reduced to zero pixels. Neither border alone is suspicious at moderate widths — a 20% left border and a 25% right border each appear minor — but together they consume 45% of the container width, forcing a narrow column. At higher values (e.g., both at 40%), the combined 80% consumption makes the content area only 20% of the container width, making long permission strings unreadable.
/* Combined bilateral inline border attack */
.consent-text {
border-inline-start: 30% solid #ffffff !important; /* 30% of container width */
border-inline-end: 30% solid #ffffff !important; /* 30% of container width */
/* content width = 100% - 30% - 30% = 40% of container
"read, write, delete, execute (all files)" in 40% of container:
- At 400px container: content = 160px → text wraps into ≥3 lines
- With overflow:hidden + fixed height: last 2 lines clip
- Clipped: "delete, execute (all files)" — the highest-risk permissions */
overflow: hidden !important;
max-height: 2em !important; /* show only 1–2 lines */
}
/* Neither border individually trips a single-side threshold;
the attack is the combination. Detection must sum both:
combined = borderInlineStartWidth + borderInlineEndWidth
flag when combined > 40% of container clientWidth */
Attack 4: JS mousedown injection
/* Mousedown-only border-inline injection */
(function() {
document.querySelectorAll('.approve-btn, [data-action="allow"]').forEach(btn => {
btn.addEventListener('mousedown', () => {
document.querySelectorAll('.consent-text, .permission-disclosure').forEach(el => {
const w = el.clientWidth;
el.style.borderInlineStartWidth = `${Math.round(w * 0.55)}px`;
el.style.borderInlineStartStyle = 'solid';
el.style.borderInlineStartColor = getComputedStyle(el.parentElement).backgroundColor;
el.style.overflow = 'hidden';
});
}, { passive: true });
btn.addEventListener('mouseup', () => {
document.querySelectorAll('.consent-text, .permission-disclosure').forEach(el => {
el.style.borderInlineStartWidth = '';
el.style.borderInlineStartStyle = '';
el.style.borderInlineStartColor = '';
el.style.overflow = '';
});
}, { passive: true });
});
})();
Detection summary
border-inline-start-width or border-inline-end-width exceeds 30% of container clientWidth on a consent text container — significant inline-space collapse.
border-inline-start-width + border-inline-end-width ≥ 50% of container clientWidth — bilateral width annihilation.
dir="rtl" on a consent container that does not contain RTL language content — direction spoofing to swap logical border sides.
border-inline-start-color or border-inline-end-color matches computed background — background-matching invisible border consuming inline space.
SkillAudit audits all CSS logical border properties — including border-inline-start, border-inline-end, border-block-start, and border-block-end — in all direction and writing-mode contexts. Physical-only border checks miss logical-property attacks in RTL or vertical writing mode contexts. Run a free audit on your MCP server.