Security reference · CSS injection · Font variants · Small caps · Consent hiding

MCP server CSS font-variant consent security

CSS font-variant properties change which glyph forms are rendered — small capitals, oldstyle numerals, unicase mixtures, and alternate stylistic sets — without changing the nominal font-size. WCAG and most accessibility checks examine font-size as the legibility proxy; font-variant-caps: all-small-caps renders glyphs at approximately 70% of the nominal font-size while leaving that property unchanged. Four attack patterns: all-small-caps glyph size collapse below threshold; oldstyle-nums descenders clipped by fixed-height containers; unicase mixed letterform confusion; and small-caps combined with already-small font-size for compounded sub-threshold rendering.

font-variant properties and rendered glyph height

font-variant valueAffectsRendered glyph height vs. font-sizeAttack vector
font-variant-caps: small-capsLowercase letters → small capitals~70% of font-size for lowercase glyphsSub-threshold rendering for lowercase-heavy consent
font-variant-caps: all-small-capsAll letters → small capitals~70% of font-size for all glyphsAll consent text rendered at sub-threshold glyph size
font-variant-caps: unicaseMixed upper/lower formsVariable — font-dependentUnpredictable letterform mixtures reduce keyword recognition
font-variant-numeric: oldstyle-numsDigit glyphs → oldstyle formsDescenders extend 20-30% below baselineDescenders clipped in fixed-height line boxes

font-variant affects rendered glyph dimensions, not font-size: getComputedStyle(el).fontSize returns the nominal font-size — e.g., 16px. With font-variant-caps: all-small-caps, the rendered capital glyph height is approximately 11px (70% of 16px). WCAG 1.4.4 Resize Text and minimum-size guidelines reference the CSS font-size value, not the rendered glyph height. A consent element styled with all-small-caps at 12px nominal size would report 12px font-size (passing a 9px minimum threshold) while rendering at approximately 8.4px glyph height — below threshold.

Attack 1: font-variant-caps: all-small-caps — rendered glyph size below threshold

all-small-caps renders uppercase and lowercase letters using the font's small-capitals glyphs. These glyphs have cap-heights of approximately 70% of the full capital-letter height. For 16px text, the consent glyphs render at approximately 11px cap-height. For 12px text (a common secondary-text size), small-caps glyphs render at approximately 8px — below the practical legibility threshold. Standard audits check font-size: 12px and pass; the rendered glyph is at 8px:

/* Malicious CSS — SA-CSS-FV-001 */
.mcp-consent-text {
  font-size: 12px; /* Reported font-size — passes a 9px minimum check */
  font-variant-caps: all-small-caps;
  /* Actual rendered cap-height: ~12px × 0.70 = 8.4px — below threshold */

  /* All text — including uppercase consent keywords like "GRANT", "ALLOW",
     "INSTALL" — is rendered in small-capitals glyphs at ~8px cap-height */
}

/* Why 70%? The CSS spec for font-variant-caps does not define a size ratio.
   The font designer specifies the small-cap glyph size. For most system fonts:
   - Helvetica Neue: small-caps at ~68-72% of full caps
   - Georgia: small-caps at ~65-70% of full caps
   - System-ui: varies by OS, typically 68-75%
   All produce rendered text smaller than what font-size reports. */

/* Detection: */
function detectSmallCapsConsent() {
  const findings = [];
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const fvc = getComputedStyle(el).fontVariantCaps;
    if (fvc && !['normal', 'initial', 'inherit', 'unset'].includes(fvc)) {
      const fs = parseFloat(getComputedStyle(el).fontSize);
      const effectiveSize = fs * 0.70; /* conservative estimate */
      findings.push({ id: 'SA-CSS-FV-001', severity: 'high',
        message: `Consent element has font-variant-caps: "${fvc}". Nominal font-size: ${fs}px. Estimated rendered glyph height: ~${effectiveSize.toFixed(1)}px (70% of nominal). WCAG checks font-size, not rendered glyph height. Check if effective size falls below minimum legibility threshold.` });
    }
  }
  return findings;
}

Attack 2: font-variant-numeric: oldstyle-nums — descender clipping in fixed-height containers

Oldstyle numerals (also called text figures) extend above and below the baseline — digits 0, 1, 2 have ascenders or regular height, while 3, 4, 5, 6, 7, 8, 9 have descenders that fall 20-30% below the baseline. In a consent container with overflow: hidden and a height calculated from line-height × number-of-lines, the descenders of oldstyle numerals are clipped. Permission scope numbers, version codes, and rate limits embedded in consent text are partially hidden:

/* Malicious CSS — SA-CSS-FV-002 */
.mcp-permission-list {
  font-variant-numeric: oldstyle-nums;
  /* Oldstyle numeral descenders: 3,4,5,6,7,8,9 descend below baseline */
  /* Descender depth: typically 15-30% of font-size below baseline */

  /* Container height precisely matches line-height × lines */
  overflow: hidden;
  height: calc(1.4em * 3); /* exactly 3 lines at 1.4 line-height */
  /* Descenders of the bottom line's numerals are clipped */
}

/* Example consent text:
   "This MCP server will read up to 50,000 messages per day"
   "and can write to 3 folders in your file system"
   "at a rate of 9 operations per second"
   ↑ The "9" in "9 operations" is the last line — its descender clips.
   The numeral is still readable but partially obscured.
   More damaging: "read 50,000 messages" — the "9" descender clips too. */

/* A more aggressive variant: small font-size + oldstyle-nums + overflow:hidden */
.mcp-rate-limit-note {
  font-size: 11px;
  font-variant-numeric: oldstyle-nums;
  overflow: hidden;
  /* At 11px, descenders may be clipped to the point where 5,6,9 are
     ambiguous — "read 56 files per second" vs "read 58 files per second" */
}

Attack 3: font-variant-caps: unicase — mixed letterform confusion

unicase produces a mixture of uppercase and lowercase glyph forms in an unpredictable, font-dependent pattern. For consent keywords that users rely on pattern-recognition to identify ("GRANT", "ALLOW", "INSTALL"), unicase rendering produces an unfamiliar mixed-case presentation that slows reading and recognition. Unlike all-caps or lowercase which are predictable, unicase output varies by font:

/* Malicious CSS — SA-CSS-FV-003 */
.mcp-consent-header {
  font-variant-caps: unicase;
  /* Unicase: produces mixed uppercase and lowercase forms from the font's
     unicase feature table. The exact output is font-defined.
     For many web fonts, unicase produces a visually noisy mixture
     that impairs rapid keyword recognition. */
}

/* Example: "GRANT READ ACCESS TO YOUR FILES"
   With unicase in some fonts renders similar to:
   "GRaNt ReaD aCCeSs TO YoUr FiLeS"
   The brain's pattern-recognition for known consent keywords is disrupted.
   The text is technically readable but requires more cognitive effort. */

/* Why this matters for security:
   Users of MCP install dialogs often skim consent text quickly.
   Unicase rendering forces slow serial letter-by-letter reading
   instead of word-shape recognition, increasing the chance of
   misreading scope keywords like "ALL", "FULL", "WRITE". */

/* Detection: */
function detectUnicaseConsent() {
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const fvc = getComputedStyle(el).fontVariantCaps;
    if (fvc === 'unicase' || fvc === 'petite-caps' || fvc === 'all-petite-caps') {
      findings.push({ id: 'SA-CSS-FV-003', severity: 'medium',
        message: `Consent element has font-variant-caps: "${fvc}". This produces non-standard mixed or reduced glyph forms that may impair recognition of consent keywords. font-size reports the nominal value — rendered glyph forms differ.` });
    }
  }
  return findings;
}

Attack 4: font-variant: small-caps combined with small font-size — compounded sub-threshold rendering

When font-variant: small-caps (shorthand, affecting lowercase letters) is combined with a font-size that is already near the minimum legibility threshold, the compound effect pushes lowercase glyph rendering well below threshold. An attacker sets font-size: 10px (above a 9px minimum) and font-variant: small-caps. The uppercase letters render at 10px; the lowercase letters render at approximately 7px — clearly below threshold but reported as 10px font-size:

/* Malicious CSS — SA-CSS-FV-004 */
.mcp-consent-fine-print {
  font-size: 10px; /* passes a 9px minimum threshold check */
  font-variant: small-caps; /* shorthand → font-variant-caps: small-caps */

  /* Lowercase letter glyph height: ~10px × 0.70 = 7px
     WCAG: checks font-size (10px — passes)
     Rendered lowercase: 7px — fails WCAG 1.4.4 readability in practice */
}

/* Compound attack chain:
   - font-size: 10px       → passes ≥9px minimum check
   - small-caps            → lowercase at ~7px
   - color: #555            → 5.5:1 contrast on white (barely passes AA for large text,
                              does NOT meet AA 4.5:1 for normal text at 7px rendered)
   - line-height: 1.1      → lines overlap at this glyph size
   Each factor individually might be flagged; combined they create a borderline
   that defeats automated checks while remaining technically "valid". */

/* Detection: check getComputedStyle().fontVariant + fontSize together */
function detectCompoundSmallCaps() {
  const CONSENT = /consent|disclosure|terms|privacy|grant.*access|agree.*install/i;
  const findings = [];
  for (const el of document.querySelectorAll('*')) {
    if (!CONSENT.test(el.textContent?.substring(0, 300) || '')) continue;
    const s = getComputedStyle(el);
    const fvc = s.fontVariantCaps;
    if (!fvc || fvc === 'normal') continue;
    const fs = parseFloat(s.fontSize);
    /* small-caps affects lowercase — effective size for lowercase glyphs */
    if (fvc === 'small-caps' && fs < 14) {
      /* 14px × 0.70 = 9.8px — effective lowercase near threshold */
      findings.push({ id: 'SA-CSS-FV-004', severity: 'medium',
        message: `Consent element has font-variant-caps: "small-caps" with font-size: ${fs}px. Estimated lowercase rendered glyph height: ~${(fs * 0.70).toFixed(1)}px. Combined effect may push rendered size below minimum legibility threshold even though font-size passes.` });
    }
  }
  return findings;
}

font-variant attacks evade font-size minimum checks: WCAG success criterion 1.4.4 defines text resize requirements in terms of CSS font-size. Browser accessibility audits and most automated tools check getComputedStyle(el).fontSize. font-variant-caps renders glyphs at a fraction of the nominal font-size without changing that property. A consent element at font-size: 12px; font-variant-caps: all-small-caps passes a 10px minimum check while rendering at ~8.4px glyph height. Accurate detection requires checking both fontVariantCaps and applying the approximately 0.70 multiplier to estimate effective rendered glyph size.

SkillAudit findings for CSS font-variant consent attacks

HighSA-CSS-FV-001 — Consent element has font-variant-caps: all-small-caps. All glyphs (uppercase and lowercase) render at approximately 70% of the nominal font-size. If the effective glyph height falls below the minimum legibility threshold after applying the 70% factor, this constitutes a font-size evasion attack. getComputedStyle(el).fontSize reports the nominal value and will not trigger font-size minimum checks.
MediumSA-CSS-FV-002 — Consent element has font-variant-numeric: oldstyle-nums with overflow: hidden on a fixed-height ancestor. Oldstyle numerals have descenders extending 15-30% below the baseline. In a container sized to the line-height without accounting for descenders, numeral descenders are clipped — numbers in consent text (scope counts, rate limits, version identifiers) may be partially illegible.
MediumSA-CSS-FV-003 — Consent element has font-variant-caps: unicase or petite-caps. These values produce non-standard, font-dependent mixed glyph forms that impair rapid pattern-recognition of consent keywords. Unicase output is unpredictable across fonts — the exact letterform mixture is not specified by CSS and varies per typeface.
MediumSA-CSS-FV-004 — Consent element has font-variant: small-caps combined with a small font-size (below 14px). The compound effect renders lowercase glyphs at approximately 70% of an already-small nominal size, falling below the practical legibility threshold. The nominal font-size passes minimum threshold checks; the effective rendered size does not.

Related MCP consent attack research

SkillAudit's consent audit checks fontVariantCaps, fontVariantNumeric, and fontVariantAlternates on consent elements and applies the appropriate glyph-size multiplier to estimate effective rendered glyph height — catching font-variant attacks that evade standard font-size minimum checks. Paste your MCP server URL at skillaudit.dev to scan for SA-CSS-FV findings.