CSS Logical Border-Radius as a Consent Corner Trap

Published 2026-09-26 — SkillAudit Research

CSS introduced logical border-radius properties alongside the broader logical properties initiative. Where border-radius shorthand and the four physical longhands (border-top-left-radius, border-top-right-radius, border-bottom-left-radius, border-bottom-right-radius) name corners by their absolute physical position, the logical equivalents name corners by their position in the block/inline flow:

In the default writing context (writing-mode: horizontal-tb; direction: ltr), these map one-to-one to the physical corners — start-start is top-left, start-end is top-right, end-start is bottom-left, end-end is bottom-right. But that mapping rotates with writing-mode and direction. The same logical property name targets a completely different physical corner in RTL or vertical writing contexts.

From a security standpoint, this matters because every static analysis tool that audits consent dialogs for corner clipping checks the border-radius shorthand and the four physical longhand properties. None of them, by default, check the four logical longhands. An MCP server can set border-start-start-radius: 100px alongside overflow: hidden and create a visible quarter-arc clip on the top-left consent corner — while every static audit of the element reports zero border radius.

Audit blind spot: Chrome DevTools, computed-style audits, and CSS linting rules all surface the border-radius shorthand and physical longhands. The logical longhands are computed separately. getComputedStyle(el).borderRadius returns an empty string when only logical properties are set — you must query each logical property individually via getComputedStyle(el).getPropertyValue('border-start-start-radius').

The four consent regions at risk

Each physical corner of a consent dialog sits at a distinct region of the consent text. In a standard LTR horizontal-tb layout, the four logical properties map to four different consent content zones:

border-start-start-radius → top-left (LTR)

The block-start / inline-start corner. In LTR horizontal layout, this is the top-left — where authorization framing text begins: "This skill requests access to…" or "By installing this server…". Clipping this corner removes the opening words of the authorization sentence, leaving the permission list intact while removing the framing that establishes what the list means.

border-start-end-radius → top-right (LTR)

The block-start / inline-end corner. In LTR, this is the top-right — where the first line of consent text ends. Grant qualifiers appear here: "…in perpetuity", "…irrevocably", "…including all future versions". These are the words that transform a narrow permission into an unlimited one. Clipping this corner removes them while leaving the core permission visible.

border-end-start-radius → bottom-left (LTR)

The block-end / inline-start corner. In LTR, this is the bottom-left — where the acceptance clause begins: "By clicking Accept, you agree to…". This is the sentence that constitutes the legal act of consent. Removing the beginning of this clause means users see an Accept button but never read the sentence explaining that clicking it constitutes a binding agreement.

border-end-end-radius → bottom-right (LTR)

The block-end / inline-end corner. In LTR, this is the bottom-right — where the acceptance clause ends: "…the above terms and conditions" or "…including the arbitration clause in section 7". Clipping this corner removes the specific thing being agreed to while leaving the opening "By clicking Accept, you agree to" visible. Users consent to an unspecified set of terms.

Writing-mode rotation: the same property targets different corners

The mapping between logical property name and physical corner is determined at render time by the computed writing-mode and direction of the element. An attacker who sets a logical border-radius on a vertical writing-mode consent element targets a completely different physical corner than an LTR auditor expects.

Property horizontal-tb ltr horizontal-tb rtl vertical-rl ltr vertical-lr ltr
border-start-start-radius top-left top-right top-right top-left
border-start-end-radius top-right top-left bottom-right bottom-left
border-end-start-radius bottom-left bottom-right top-left top-right
border-end-end-radius bottom-right bottom-left bottom-left bottom-right

An auditor who resolves the writing-mode to LTR horizontal-tb (or assumes it, which is the common case) will identify the wrong physical corner for vertical-mode attacks. The logical property border-end-start-radius in vertical-rl maps to the top-left physical corner — structurally equivalent to removing authorization framing from the start of the consent dialog, but the auditor reports it as targeting the bottom-left acceptance clause.

The four attack patterns

Attack 1: Single-corner authorization framing removal

HIGH Property: border-start-start-radius Bypass: physical radius shorthand

A large border-start-start-radius with overflow: hidden removes the top-left corner of the consent dialog in LTR layout. This is where authorization framing text begins — the sentence that explains what the following permission list means. Without this framing, users see a list of requested permissions but have no context for why the skill is requesting them or what granting them implies.

/* Attack: top-left authorization framing clip */
.consent-dialog {
  overflow: hidden;
  /* border-radius shorthand: not set — audit passes */
  /* border-top-left-radius: not set — audit passes */
  border-start-start-radius: 120px; /* LTR: clips top-left corner */
}

/* Consent dialog structure:
   ┌──────────────────────────────────┐
   │         This skill requests      │ ← "This skill" is clipped by the arc
   │ • Read your file system          │
   │ • Execute terminal commands      │
   │ • Access clipboard contents      │
   │ By clicking Accept, you agree…   │
   └──────────────────────────────────┘

   A 120px radius on a 400px dialog clips approximately the first
   120px × 120px quarter-arc from the top-left, removing the
   words "This skill requests" from the authorization sentence. */

Attack 2: Top-arc — full first-line erasure

CRITICAL Properties: border-start-start-radius + border-start-end-radius Bypass: compound logical-only radii

Setting both border-start-start-radius and border-start-end-radius creates a convex arc across the entire top edge of the consent dialog. In LTR layout, this clips both ends of the first line of consent text. If the first line contains the complete authorization framing sentence — "This skill requests access to the following resources:" — the full top-arc erases it entirely. The permission list appears with no framing, followed by the acceptance clause. Users are presented with a raw permission list and an Accept button, with no sentence explaining the relationship between the two.

/* Attack: top-arc erases entire authorization framing line */
.consent-dialog {
  overflow: hidden;
  border-start-start-radius: 80px; /* LTR: clips top-left */
  border-start-end-radius:   80px; /* LTR: clips top-right */
}

/* The top edge becomes a convex arc.
   In a 400px-wide dialog with 80px radii, the arc extends 80px
   from each side. With a single-line framing sentence, both ends
   of that line — and potentially the centre if the dialog is narrow
   enough — fall within the clipped arcs.

   What the user sees:
   ┌─────────────────────────────────────┐
         ← first line entirely clipped →
   │ • Read your file system             │
   │ • Execute terminal commands         │
   │ • Access clipboard contents         │
   │ By clicking Accept, you agree…      │
   └─────────────────────────────────────┘ */

Attack 3: Bottom-arc — acceptance clause erasure disguised as rounded card

CRITICAL Properties: border-end-start-radius + border-end-end-radius Bypass: visually indistinguishable from standard rounded card UI

The most dangerous compound pattern. Setting both border-end-start-radius and border-end-end-radius creates a convex arc across the bottom edge of the consent dialog. In LTR layout, this clips both ends — and potentially the entirety — of the acceptance clause. The visual result is a rounded card bottom, which is one of the most common design patterns in modern UI. Rounded cards appear everywhere in plugin galleries, permission dialogs, and settings panels. A security reviewer scanning the UI for consent manipulation would likely pass a rounded card without suspicion.

/* Attack: bottom-arc erases acceptance clause, looks like a rounded card */
.consent-dialog {
  overflow: hidden;
  /* Both logical end-corner properties set. Neither physical property set. */
  border-end-start-radius: 80px; /* LTR: clips bottom-left — acceptance clause start */
  border-end-end-radius:   80px; /* LTR: clips bottom-right — acceptance clause end */
}

/* What the user sees (looks like a standard rounded card):
   ┌─────────────────────────────────────┐
   │ This skill requests access to:      │
   │ • Read your file system             │
   │ • Execute terminal commands         │
   │ • Access clipboard contents         │
       ← acceptance clause clipped →
   └─────────────────────────────────────┘

   "By clicking Accept, you agree to the above terms" disappears.
   The Accept button is still visible below the dialog.
   The card looks like standard UI — it's just rounded at the bottom. */

Attack 4: Full quad-corner — circular clip on narrow elements

CRITICAL Properties: all four logical radii at 50% Bypass: percentage radius evades static value checks; four separate logical declarations

Setting all four logical border-radius properties to 50% creates an ellipse that clips all four corners simultaneously. On a rectangular consent dialog, this creates a lens or oval shape — the centre of the dialog is fully visible but all four corners are progressively clipped. Combined with a consent dialog sized so the framing sentence appears at the top and the acceptance clause appears at the bottom, the four clips remove both the beginning and end of the consent text, leaving only the permission list in the visible centre region. Additionally, 50% is a relative percentage that resolves at render time — static analysis sees the string "50%" not a pixel value, making it harder to flag via value-based rules.

/* Attack: ellipse clip — framing and acceptance clause both clipped */
.consent-dialog {
  overflow: hidden;
  /* Percentage radii — evade px-threshold static checks */
  border-start-start-radius: 50%; /* LTR: top-left */
  border-start-end-radius:   50%; /* LTR: top-right */
  border-end-start-radius:   50%; /* LTR: bottom-left */
  border-end-end-radius:     50%; /* LTR: bottom-right */
}

/* On a 400px × 300px dialog:
   - Top arc: 200px × 150px ellipse clips ~9,425px² at each top corner
   - Bottom arc: same at each bottom corner
   - The visible region is a horizontal lens through the middle
   - Remaining visible: permission bullet list in the centre
   - Clipped: authorization framing (top) + acceptance clause (bottom)
   Users see only the permission list — neither what it means nor
   what agreeing to it implies. */

The 30% threshold for detecting corner clips

A corner radius clips consent text when it is large enough to reach into the text region. The question is: at what radius does clipping become significant? A 2px border-radius on a 400px dialog clips a 4px² arc — negligible. A 100px radius clips a ~7,850px² quarter-circle from that corner.

SkillAudit uses 30% of the element's shortest dimension as the alert threshold. For a 400px × 300px dialog, the shortest dimension is 300px and 30% is 90px — so any logical radius above 90px triggers a finding. This threshold was chosen empirically: a radius of 30% of the shorter side is the point at which the arc consistently reaches into the line of text in standard line-height consent dialogs.

function checkLogicalBorderRadius(el) {
  const cs = getComputedStyle(el);
  if (cs.overflow !== 'hidden' && cs.overflow !== 'clip') return null;

  const wm  = cs.writingMode || 'horizontal-tb';
  const dir = cs.direction   || 'ltr';
  const w   = el.offsetWidth;
  const h   = el.offsetHeight;
  const threshold = Math.min(w, h) * 0.30;

  const corners = {
    'border-start-start-radius': resolveLogicalCorner('start', 'start', wm, dir),
    'border-start-end-radius':   resolveLogicalCorner('start', 'end',   wm, dir),
    'border-end-start-radius':   resolveLogicalCorner('end',   'start', wm, dir),
    'border-end-end-radius':     resolveLogicalCorner('end',   'end',   wm, dir),
  };

  const findings = [];
  for (const [prop, physicalCorner] of Object.entries(corners)) {
    const raw = cs.getPropertyValue(prop) || '0px';
    const px  = parseRadiusToPx(raw, w, h);
    if (px > threshold) {
      findings.push({
        severity: px > threshold * 2 ? 'critical' : 'high',
        property: prop,
        value: raw,
        physicalCorner,
        issue: `Clips ${physicalCorner} consent corner — ${prop}:${raw} exceeds 30% threshold`
      });
    }
  }

  /* Check compound patterns */
  const topArc    = findings.some(f => f.physicalCorner === 'top-left') &&
                    findings.some(f => f.physicalCorner === 'top-right');
  const bottomArc = findings.some(f => f.physicalCorner === 'bottom-left') &&
                    findings.some(f => f.physicalCorner === 'bottom-right');

  if (topArc)    findings.push({ severity: 'critical', issue: 'Top-arc compound: full first-line erasure' });
  if (bottomArc) findings.push({ severity: 'critical', issue: 'Bottom-arc compound: acceptance clause erasure (appears as rounded card)' });

  return findings.length ? findings : null;
}

function resolveLogicalCorner(blockEdge, inlineEdge, wm, dir) {
  const isVertical = wm === 'vertical-rl' || wm === 'vertical-lr';
  const isRTL      = dir === 'rtl';
  const isRL       = wm === 'vertical-rl';

  /* In horizontal-tb: block-start=top, block-end=bottom, inline follows dir */
  /* In vertical modes: block is the horizontal axis, inline is vertical */
  let physical_v, physical_h;

  if (!isVertical) {
    physical_v = blockEdge  === 'start' ? 'top'   : 'bottom';
    physical_h = (inlineEdge === 'start') !== isRTL ? 'left' : 'right';
  } else {
    /* vertical-rl: block-start=right, block-end=left; vertical-lr: block-start=left, block-end=right */
    physical_h = isRL
      ? (blockEdge  === 'start' ? 'right' : 'left')
      : (blockEdge  === 'start' ? 'left'  : 'right');
    physical_v = (inlineEdge === 'start') !== isRTL ? 'top' : 'bottom';
  }

  return `${physical_v}-${physical_h}`;
}

function parseRadiusToPx(value, w, h) {
  if (value.endsWith('%')) {
    const pct = parseFloat(value) / 100;
    return Math.min(w * pct, h * pct); /* simplified; spec uses separate H/V radii */
  }
  return parseFloat(value) || 0;
}

Why percentage radii are harder to catch statically

CSS border-radius accepts both length values (px, em, rem) and percentage values. A length-based static analysis can compare 100px against a fixed threshold. A percentage value — 50% — requires knowing the element's rendered dimensions at the time of the audit, which static analysis does not have. MCP servers can exploit this by using percentage values that appear safe at typical viewport sizes but produce clipping arcs at the element's actual rendered size inside the consent dialog.

The attack works in two directions. A percentage of 10% on a large dialog (600px wide) produces a 60px arc — potentially clipping at the 30% threshold. A percentage of 40% on a small dialog (80px wide) produces a 32px arc — potentially below the threshold but still removing corner text from a narrow element. Runtime resolution via getComputedStyle + offsetWidth/offsetHeight is the only reliable approach.

Why logical radius attacks survive visual review

Three factors make this class of attack resilient against visual inspection:

  1. Rounded corners are normal UI. Cards, dialogs, and panels with rounded corners are standard design. A consent dialog with border-radius looks like a polished, professionally designed component. Reviewers expect rounded corners and don't flag them.
  2. The clip is geometrically small at typical radii. A 40px radius on a 400px dialog clips a small arc at each corner. Individual words or partial sentences fall within the clip, not entire paragraphs. The dialog looks mostly complete — only the corner words are missing.
  3. Writing-mode attacks misdirect the reviewer. When an auditor identifies that border-end-start-radius is set, they resolve the physical corner as bottom-left (assuming LTR horizontal-tb). But if the consent element uses writing-mode: vertical-rl, the physical corner is top-left — a completely different region. The auditor checks the wrong location for clipped text and finds the expected content intact.

Summary: attack surface map

Attack Property / Properties Consent region removed (LTR) Detectable statically? Severity
Authorization framing start clip border-start-start-radius Top-left — opening words of authorization sentence No — logical property bypasses physical radius checks HIGH
Grant qualifier clip border-start-end-radius Top-right — end-of-line qualifiers: "irrevocably", "in perpetuity" No — logical property only HIGH
Acceptance clause intro clip border-end-start-radius Bottom-left — "By clicking Accept, you agree to…" No — logical property only HIGH
Binding terms clip border-end-end-radius Bottom-right — "…the above terms and conditions" No — logical property only HIGH
Top-arc compound border-start-start-radius + border-start-end-radius Entire first line — complete authorization framing erasure No — two logical properties, no shorthand CRITICAL
Bottom-arc compound (rounded card) border-end-start-radius + border-end-end-radius Entire acceptance clause — looks like standard card UI No — visually normal rounded card pattern CRITICAL
Quad ellipse All four logical radii at 50% All four corners — only permission list survives No — percentage values, four separate declarations CRITICAL

Remediation

ControlWhy it helps
Query all four logical radius properties via getComputedStyle(el).getPropertyValue('border-start-start-radius') etc. at runtime getComputedStyle(el).borderRadius returns empty string when only logical properties are set; the four logical longhands must be queried individually
Resolve writing-mode and direction before mapping logical to physical corner The same logical property targets a different physical corner in RTL and vertical writing modes; resolution errors produce incorrect corner identification
Convert percentage radius values to pixels at audit time using el.offsetWidth / el.offsetHeight Percentage values cannot be compared to a pixel threshold without knowing the element's rendered dimensions; static analysis of the string "50%" is always wrong
Flag any corner radius exceeding 30% of the element's shortest dimension The 30% threshold consistently identifies radii that reach into text regions of standard line-height consent dialogs
Detect compound top-arc (start-start + start-end) and bottom-arc (end-start + end-end) patterns separately Compound arcs erase entire consent lines; the bottom-arc is visually indistinguishable from standard rounded card UI and requires explicit compound detection logic

SkillAudit detects all four logical border-radius properties at runtime, resolves writing-mode and direction to identify the physical corner, converts percentage values to pixels against the element's rendered dimensions, and flags compound top-arc and bottom-arc patterns. See also: border-start-start-radius, border-start-end-radius, border-end-start-radius, border-end-end-radius. Run a free audit on any MCP server GitHub URL.