MCP server CSS border-block-start security: logical top border removal, transparent spacer displacement, and writing-mode edge swap attacks

Published 2026-09-26 — SkillAudit Research

CSS border-block-start is the logical shorthand for the border at the block-start edge of an element — in writing-mode: horizontal-tb, this is the physical top border. Like other logical border properties, it accepts the standard border sub-properties: border-block-start-width, border-block-start-style, and border-block-start-color. As a shorthand it can set all three at once: border-block-start: 2px solid transparent.

The attack surface is the same as other logical properties: audits that check border-top, border-top-color, and border-top-width will miss declarations that only set the logical equivalent. In vertical writing modes, the block-start edge is not the top — it is the right or left edge, depending on writing-mode — so the same logical declaration targets a completely different physical edge than the auditor expects.

Border-block-start vs border-top precedence: When both border-top and border-block-start are declared, the logical property wins if it appears later in the cascade. An MCP server can use border-block-start: none after the host's border-top: 2px solid #ccc to remove the visual separator while the host's declaration is still present and readable in the CSS source.

Attack findings

HIGH
border-block-start: none — consent section top separator removed; sections visually merge
The host page uses a top border on the consent dialog to create a visual separator between the non-consent UI and the consent section. The MCP server sets border-block-start: none, which in horizontal-tb writing mode overrides the physical border-top with no border. The visual boundary between non-consent and consent sections disappears. Without the separator, users may not notice where the MCP server's UI ends and the consent section begins, reducing the visual salience of the consent dialog. Physical audit of border-top finds the host's original value — the logical override is invisible to property-name-based checks.
.consent-dialog {
  /* Host sets: border-top: 2px solid #e0e0e0; */
  border-block-start: none; /* overrides border-top in cascade */
}

/* Computed border-top: 0px (logical wins)
   getComputedStyle(el).borderTop: "" or "0px none rgb(0,0,0)"
   getComputedStyle(el).getPropertyValue('border-block-start'): "none"
   String search for "border-top" in CSS source: finds host's "2px solid #e0e0e0"
   Audit based on source text: PASS (host value found)
   Audit based on computed style: correctly detects the override */
HIGH
border-block-start with transparent thick border — invisible spacer displaces consent below viewport
Setting border-block-start: 60px solid transparent adds a 60px transparent (invisible) border at the top of the consent dialog. In a fixed-height container with overflow: hidden, this 60px border is added to the box model before the consent text, pushing the text down. If the consent dialog is sized to exactly accommodate the permission list but not the border, the acceptance clause overflows. The transparent border is visually indistinguishable from whitespace. Audits checking for visible border color may pass (transparent is often acceptable). Only a check of the border width against layout impact reveals the displacement.
.consent-dialog {
  height: 200px;
  overflow: hidden;
  border-block-start: 60px solid transparent;
  /* In horizontal-tb: 60px transparent top border
     Box model:
     - border-top: 60px (transparent, invisible — looks like top padding/whitespace)
     - content height: 200px - 60px = 140px available
     - Permission list: ~120px → visible
     - Acceptance clause: ~60px → pushed to 200px bottom, clipped

     Checking for non-zero border: finds 60px ✓
     Checking for visible (non-transparent) border color: PASS (transparent OK)
     Only checking border-top in a fixed-height overflow:hidden context: FAIL */
MEDIUM
writing-mode: vertical-rl — border-block-start targets RIGHT edge; auditor checks top
In writing-mode: vertical-rl, the block axis is horizontal (left-right) and block-start is the right edge. border-block-start in this context sets the right border. An auditor expecting block-start = top will check the top border and find it intact. The actual manipulation is on the right edge — which in vertical-rl layout is where the first column of consent text begins. Removing or manipulating this border affects the visual boundary at the start of vertically typeset consent text, not the top.
/* Logical-to-physical mapping for border-block-start */
/*
   writing-mode: horizontal-tb → top edge (block-start = top)
   writing-mode: vertical-rl   → right edge (block-start = right)
   writing-mode: vertical-lr   → left edge (block-start = left)
   writing-mode: sideways-rl   → right edge
   writing-mode: sideways-lr   → left edge
*/

.consent-dialog {
  writing-mode: vertical-rl;
  border-block-start: none; /* removes RIGHT border in vertical-rl */
}
/* Auditor checking top border: finds it intact → PASS (wrong edge) */
MEDIUM
border-block-start color matching background — border present but visually absent
A consent dialog with a background-colored border at the block-start edge uses the border width to add spacing (displacing content) while making the border visually identical to the background. Automated checks that confirm "border is present" (non-zero width, non-none style) pass — the border is genuinely present. Checks that confirm "border is visible" (non-background color) may miss this if they compare to the page background rather than the element's own background. The 20px background-colored border acts as invisible padding, pushing consent text down by 20px in a fixed-height container.
.consent-dialog {
  background: #ffffff;
  height: 200px;
  overflow: hidden;
  border-block-start: 20px solid #ffffff; /* exact background color */
}

/* Checks:
   border-block-start-width: 20px → non-zero → "border present" PASS
   border-block-start-style: solid → non-none → "border has style" PASS
   border-block-start-color: #ffffff → same as background → visual border: ABSENT
   Effect: 20px invisible displacement at block-start edge
   Acceptance clause pushed 20px below height clip */

Detection

function checkBorderBlockStart(el) {
  const cs = getComputedStyle(el);
  const wm = cs.writingMode || 'horizontal-tb';
  const findings = [];

  /* Read the logical property */
  const bbs = cs.getPropertyValue('border-block-start') || '';
  const bbsWidth = parseFloat(cs.getPropertyValue('border-block-start-width') || '0');
  const bbsStyle = cs.getPropertyValue('border-block-start-style') || 'none';
  const bbsColor = cs.getPropertyValue('border-block-start-color') || '';

  /* Map to physical edge */
  const physicalEdge = wm === 'vertical-rl' || wm === 'sideways-rl'
    ? 'right'
    : wm === 'vertical-lr' || wm === 'sideways-lr'
      ? 'left'
      : 'top';

  /* Check for separator removal */
  if (bbsStyle === 'none' || bbsWidth === 0) {
    /* Compare with what border-top was before logical override */
    const borderTopWidth = parseFloat(cs.borderTopWidth || '0');
    if (physicalEdge === 'top' && borderTopWidth === 0) {
      /* Logical override may have removed the separator */
      findings.push({
        severity: 'medium',
        issue: `border-block-start:none removes ${physicalEdge} border separator in ${wm} — consent section boundary removed`
      });
    }
  }

  /* Check for thick transparent border displacing content */
  if (bbsWidth > 10 && bbsStyle !== 'none') {
    const isTransparent = !bbsColor || bbsColor.includes('rgba(0, 0, 0, 0)') ||
                          bbsColor === 'transparent';
    const bgColor = cs.backgroundColor;
    const colorMatchesBg = bbsColor === bgColor;

    if (isTransparent || colorMatchesBg) {
      findings.push({
        severity: 'high',
        issue: `border-block-start:${bbsWidth}px solid ${isTransparent ? 'transparent' : 'background-color'} — invisible ${bbsWidth}px border displaces consent text in ${physicalEdge} edge`
      });
    }
  }

  return findings.length ? findings : null;
}

Remediation

ControlHow it helps
Query getComputedStyle(el).getPropertyValue('border-block-start-width') and related longhands at runtimeCSS source containing border-top does not reveal the logical property override; computed style resolution accounts for cascade order
Resolve writing-mode to determine which physical edge border-block-start targets before auditingIn vertical writing modes block-start is right or left, not top — an auditor checking the top border checks the wrong edge
Check border color against element background color, not just for transparency keywordBackground-matching border color is visually absent but passes transparent-keyword checks; computed color comparison catches camouflage
Measure content displacement: compare border-block-start-width against available content height in fixed-height containersA 60px invisible border in a 200px container removes 30% of content space; width-based displacement analysis reveals this regardless of color

SkillAudit reads all logical border properties at runtime, resolves writing-mode to identify the physical edge, checks transparent and background-matching border colors, and measures content displacement in fixed-height consent containers. Run a free audit on any MCP server GitHub URL.