Security Guide

MCP server CSS border-inline-color security — background-matching inline border hiding, dir=rtl physical-side swap, oklch near-background evasion, JS mousedown injection

The CSS border-inline-color property sets the color of both inline-axis borders (border-inline-start-color and border-inline-end-color) without modifying their width or style. Because inline borders govern the left and right edges of an element in a left-to-right horizontal layout, a background-matching inline border color combined with a pre-set large width collapses the available inline space — squeezing consent text into zero or near-zero width — while remaining visually invisible. An additional dimension of evasion comes from dir attribute changes: in an RTL context, border-inline-start maps to the physical right edge, not the left, so physical-side audits check the wrong property.

CSS border-inline-color — property overview

The border-inline-color shorthand is equivalent to setting border-inline-start-color and border-inline-end-color to the same value. It does not affect border-inline-width or border-inline-style. In writing-mode: horizontal-tb with dir=ltr (the default), the inline axis is horizontal: border-inline-start maps to the physical left border and border-inline-end to the physical right. With dir=rtl applied to the element or an ancestor, these mappings reverse: border-inline-start = physical right, border-inline-end = physical left. Related properties: border-inline shorthand, border-block-color.

Attack 1: background-matching border-inline-color hiding inline width collapse

When a consent container has a large border-inline-start-width or border-inline-end-width set (collapsing the available width for text), injecting a matching background color via border-inline-color makes the width-consuming border invisible. The content collapses to a narrow strip that clips the consent text, but no visible border signals the attack to the user or to a security audit that only checks for visible border colors.

/* Stage 1: establish large inline border (color = currentColor = dark text color → visible) */
.consent-body {
  border-inline-start-width: 120px;
  border-inline-start-style: solid;
  /* border-inline-start-color defaults to currentColor (dark) — border is visible */
}

/* Stage 2: color injection makes border invisible while maintaining width effect */
.consent-body {
  border-inline-color: var(--dialog-bg, #ffffff) !important;
  /* Now: 120px left border is white on white — invisible
     Available text width: (container-width - 120px) px
     If container is 180px: only 60px for consent text → severe clipping
     scrollWidth > clientWidth → detectable via overflow
     textContent: full text still present in DOM              */
}

/* Inline space calculation:
   clientWidth = 180px (border-box, unchanged)
   border-inline-start: 120px white
   border-inline-end:   0px (or also 120px if shorthand)
   padding-inline: 0px (assume)
   content-width = 180 - 120 - 0 = 60px ← text wraps aggressively or clips

   Detection:
   - Compare border-inline-*-color luminance to background luminance
   - Check scrollWidth > clientWidth (overflow clipping)
   - Check clientWidth - borderInlineStartWidth - borderInlineEndWidth < 80px */

Inline collapse is harder to detect than block collapse. A block-axis height collapse produces scrollHeight > clientHeight, which is a well-known overflow signal. An inline-axis width collapse is less commonly checked: most text overflow is intentional (truncation), and consent text that overflows inline is often clipped by overflow: hidden without any visible sign. Audits should explicitly check available inline width (content width after subtracting logical inline borders) on consent containers, not just block-axis height.

Attack 2: dir=rtl swap — inline-start appears on opposite physical side

Setting dir="rtl" on the consent container (or an ancestor) swaps the physical mapping of border-inline-start and border-inline-end. In RTL context, border-inline-start becomes the physical right border. A security audit that reads getComputedStyle(el).borderLeft to check for inline-start border presence will see a zero-width left border — the injected border is now on the right, under the logical property name. The border-inline-color injection can match the background regardless of direction, but the directional confusion alone misdirects audits to the wrong physical side.

/* Exploit: set dir=rtl, then inject border-inline-start (now physical right) */
/* 1. HTML: inject dir=rtl on consent ancestor */
consentDialog.setAttribute('dir', 'rtl');

/* 2. CSS: border-inline-start now = physical RIGHT border */
.consent-body {
  border-inline-start-width: 150px !important;  /* physical right border, 150px */
  border-inline-start-style: solid !important;
  border-inline-color: #f0f0f0 !important;       /* near-white — matches bg */
}

/* Security audit checking physical borderLeft:
   getComputedStyle(el).borderLeftWidth === '0px'  ← passes (no left border)
   getComputedStyle(el).borderRightWidth === '0px' ← passes (physical right accessed via wrong property name)

   Correct check:
   getComputedStyle(el).getPropertyValue('border-inline-start-width') === '150px' ← detected

   dir can be set on any ancestor — check the whole ancestor chain for dir=rtl
   and adjust expected physical-side interpretation accordingly. */

Physical property names are unreliable after a dir change. getComputedStyle(el).borderLeft always returns the physical left border value regardless of writing direction. To detect logical border attacks, audits must read logical property names via getPropertyValue('border-inline-start-width') and getPropertyValue('border-inline-end-width'), not their physical equivalents. The dir attribute on any ancestor is sufficient to flip the mapping.

Attack 3: oklch near-background color — evading computed-style string comparison

A border-inline-color specified in the oklch() color space with lightness near 1.0 and very low chroma renders as perceptually near-white but serializes to a browser-specific string that does not match 'white', '#ffffff', or 'rgb(255, 255, 255)' in a simple string equality check. Because border-inline-color affects only color — not width — the inline width collapse is still occurring even if the color comparison audit fails to recognize the near-white value as background-matching.

/* oklch near-background border-inline-color */
.consent-container {
  border-inline-color: oklch(0.98 0.003 110) !important;
  /* Perceptual: very light warm white — indistinguishable from #fafafa
     Serialized by browser: might be 'color(display-p3 0.965 0.964 0.959)'
     Not equal to 'white', 'rgb(255,255,255)', or background color string

     Defense must parse computed color to sRGB floats and compare luminance:
     const L_border = relativeLuminance(...parseRGB(computedBorderColor));
     const L_bg     = relativeLuminance(...parseRGB(computedBgColor));
     if (Math.abs(L_border - L_bg) < 0.04) flag();                          */
}

/* Note: if the dialog background is itself specified in oklch or color(),
   browser serialization may normalize both to the same color space,
   making comparison possible — but this is browser-dependent.
   Robust detection must convert both to sRGB and compare. */

Attack 4: JS mousedown injection of border-inline-color at click time

The same pattern documented for border-block-color mousedown injection applies to border-inline-color: a thick inline border with a visible (non-background-matching) color is present at page load — the static audit sees the width but the color is suspicious only if it matches the background, which it does not at load time. At mousedown on the approve button, the color changes to match the dialog background, collapsing the visible inline space for the 50–200 ms of the button press. At mouseup, the color reverts.

/* Mousedown: flip border-inline-color to background at click moment */
(function () {
  const CONSENT = '.consent-body, [data-consent-text]';
  const APPROVE = '.approve-btn, [data-action="allow"]';

  function collapse() {
    document.querySelectorAll(CONSENT).forEach(el => {
      const bg = getComputedStyle(
        el.closest('[class*="dialog"]') || el.parentElement
      ).backgroundColor;
      el.style.setProperty('border-inline-color', bg, 'important');
    });
  }

  function restore() {
    document.querySelectorAll(CONSENT).forEach(el => {
      el.style.removeProperty('border-inline-color');
    });
  }

  document.querySelectorAll(APPROVE).forEach(btn => {
    btn.addEventListener('mousedown', collapse, { passive: true });
    btn.addEventListener('mouseup',   restore,  { passive: true });
    btn.addEventListener('mouseleave',restore,  { passive: true });
  });
})();

Detection summary

HIGH border-inline-start-color or border-inline-end-color matches computed backgroundColor (luminance delta < 0.05) while border-inline-*-width > 0 — invisible border collapsing inline space.
HIGH dir="rtl" on consent container or ancestor combined with non-zero border-inline-start-width — physical-side mapping is swapped; check logical property names, not physical.
MEDIUM Wide-gamut oklch or color() border-inline-color that resolves to near-background luminance — evades sRGB string comparison.
MEDIUM Mousedown listener changes border-inline-color style property of consent container at click time.
LOW Available inline content width (clientWidth minus logical border widths) < 80px on a consent text element — warrants deeper color and overflow inspection.
/* Detection: logical inline border color check */
function checkBorderInlineColor(consentEl) {
  const cs   = getComputedStyle(consentEl);
  const root = consentEl.closest('[class*="dialog"]') || document.body;
  const bgL  = luminance(getComputedStyle(root).backgroundColor);

  const props = [
    ['border-inline-start-color','border-inline-start-width'],
    ['border-inline-end-color',  'border-inline-end-width'],
  ];

  props.forEach(([colorProp, widthProp]) => {
    const width = parseFloat(cs.getPropertyValue(widthProp)) || 0;
    if (width === 0) return;
    const colorStr = cs.getPropertyValue(colorProp);
    if (Math.abs(luminance(colorStr) - bgL) < 0.05) {
      console.warn('[SA] Near-bg inline border:', colorProp, colorStr, 'width:', width);
    }
  });
}

function luminance(colorStr) {
  const m = colorStr.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  if (!m) return 1; // fallback
  return 0.2126 * lin(+m[1]/255) + 0.7152 * lin(+m[2]/255) + 0.0722 * lin(+m[3]/255);
}
function lin(c) { return c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4); }

SkillAudit audits all CSS logical inline border color properties — including border-inline-color, border-inline-start-color, and border-inline-end-color — using luminance-aware comparison and direction-aware physical-side mapping. Run a free audit on your MCP server to catch inline border color evasion attacks.