Security Guide

MCP server CSS @font-face combined unicode-range + size-adjust + descent-override security — compound metric attack

A single @font-face block can combine three metric-altering descriptors: unicode-range scopes the font to a targeted codepoint subset, size-adjust reduces glyph rendering to near-invisible size, and descent-override inflates the descent metric to expand line box height. Targeting digits and currency symbols simultaneously makes price disclosures unreadable and pushes each price-bearing line into a fixed-height clip zone. Individual descriptor checks miss the compound significance — each descriptor alone appears less dangerous than the three in combination.

The compounding mechanism

Three @font-face descriptors interact to produce a compound attack that is more powerful than any single descriptor alone:

unicode-range restricts which codepoints the @font-face block applies to. When a character in an element's text matches a codepoint in the unicode-range, the browser uses this @font-face for that character. Characters outside the range use a different font source (the fallback). This creates surgical precision: only the targeted characters are affected.

size-adjust scales all glyph advance widths and metrics by a percentage. A value of 15% renders each affected glyph at 15% of its normal size — 2.4px at a 16px base font-size. The getComputedStyle(el).fontSize still returns "16px" because size-adjust does not alter the computed font-size property. WCAG minimum font size checks pass.

descent-override sets the descent metric (the space below the baseline that the line box allocates) as a percentage of the font-size. A value of 400% adds 64px of below-baseline space per line at 16px — expanding each line box by the same amount. When the targeted characters appear in a line, that entire line gains the inflated descent.

/* How the three descriptors interact — example targeting digits */

@font-face {
  font-family: 'ConsentFont';

  /* DESCRIPTOR 1: scope to digits, dollar sign, percent */
  unicode-range: U+0024, U+0025, U+0030-0039;
  /* Targets: $ % 0 1 2 3 4 5 6 7 8 9 */
  /* Any consent text with price disclosures, percentages, version numbers */
  /* uses this @font-face for those specific characters only */

  /* DESCRIPTOR 2: reduce glyph rendering to near-invisible size */
  size-adjust: 15%;
  /* At 16px base: affected glyphs render at 2.4px actual visual size */
  /* getComputedStyle(el).fontSize still returns "16px" — property unchanged */
  /* WCAG 1.4.4 minimum-size check: PASSES (spec checks computed font-size) */

  /* DESCRIPTOR 3: inflate line box via descent metric */
  descent-override: 400%;
  /* Adds 64px below-baseline per line at 16px */
  /* Any line containing a targeted character becomes ~80px tall */
  /* A consent container sized for 6 lines at normal 24px line height */
  /* (~144px) shows 1-2 lines when those lines contain digits */

  src: url('data:font/woff2;base64,...') format('woff2');
}

/* Combined effect on consent text: */
/* "Subscription: $19.99/month — 99% of features included" */
/*   → "$", "1", "9", "9", "9", "9", "9", "%" all render at 2.4px */
/*   → the price line's line box inflates to ~80px */
/*   → overflow:hidden container clips after 1-2 inflated lines */
/*   → user sees almost nothing of the pricing disclosure */

Why compound detection matters: An audit tool might flag size-adjust: 15% as "suspicious — renders glyphs very small." But without correlating it with the unicode-range (targeting price characters) and descent-override: 400% (clipping subsequent content), the full severity is not apparent. The three descriptors together constitute a CRITICAL compound attack; each individually is a HIGH or MEDIUM finding.

Attack 1 (CRITICAL): Price disclosure attack — digits and currency symbols

The most targeted variant: a @font-face block scoped to price-relevant codepoints (digits 0–9, dollar sign $, percent %, currency symbols €£¥) with size-adjust: 12% and descent-override: 350%. Every price disclosure in the consent dialog renders with near-invisible digits. The surrounding prose text is unaffected — "Subscription: /month — of features included" is readable; the numbers that complete those phrases are invisible. The compound descent inflation ensures that any line containing a price figure is ~58px taller than a normal line, pushing the next consent clause beyond the fixed-height overflow:hidden container boundary.

@font-face {
  font-family: 'UIFont';
  unicode-range: U+0024, U+0025, U+0030-0039,
                 U+00A3, U+00A5, U+20AC;  /* £ ¥ € */
  size-adjust: 12%;          /* 1.9px visual glyph at 16px base */
  descent-override: 350%;    /* +56px below baseline per targeted line */
  src: url('data:font/woff2;base64,...') format('woff2');
}

/* Consent container sized for 8 readable lines (~192px at 24px line-height) */
.consent-dialog {
  font-family: 'UIFont', sans-serif;
  max-height: 192px;
  overflow: hidden;
}

/* A consent with 3 price-bearing lines (each inflated to ~80px):
   Line 1: "$19/month"     — 80px line box (inflated)
   Line 2: "99% of users" — 80px line box (inflated)
   Line 3: (first regular clause) — 24px line box
   TOTAL before clip: 184px — only 3 content lines visible
   Remaining 5 clauses: CLIPPED */

Attack 2 (CRITICAL): Lowercase alphabet targeting — prose consent text disappears

A broader scope variant: unicode-range: U+0061-007A (lowercase Latin a–z, covering the vast majority of prose text in English-language consent dialogs) with size-adjust: 18% and descent-override: 500%. Virtually all consent text becomes unreadable — only uppercase letters, digits (if not also targeted), and punctuation remain at normal size. The consent dialog appears to contain garbled text, which users may attribute to a rendering error rather than an attack. The extreme descent inflation of 500% means each consent line is ~96px tall at 16px, reducing a 400px consent container to displaying approximately 4 lines of the ~20-line consent agreement.

@font-face {
  font-family: 'UIFont';
  unicode-range: U+0061-007A;    /* lowercase a-z — nearly all prose text */
  size-adjust: 18%;              /* 2.9px visual height at 16px base */
  descent-override: 500%;        /* +80px below baseline per line */
  src: url('data:font/woff2;base64,...') format('woff2');
}

/* Effect:
   "You agree to share your data with third parties" → unreadable
   Uppercase "You", "A" in "Agree" remain normal size
   Most of the legal prose text becomes a garbled mix of
   invisible lowercase chars and visible uppercase/punctuation */

Attack 3: Surgical punctuation targeting — sentence-ending clipping

The most subtle variant targets only sentence-ending punctuation: unicode-range: U+002C, U+002E, U+003B, U+003A (comma, period, semicolon, colon). These characters appear at the end of clauses. Applying descent-override: 600% to them means that every sentence whose last character is a period or comma gets a line box inflated by 96px. This is nearly undetectable visually because the punctuation glyphs themselves are still at full size (size-adjust: 100% — no size reduction). Only the line box below the affected characters is enlarged. A fixed-height consent container that normally shows 10 sentences may show only 4–5 because every sentence break carries a 96px hidden expansion below it.

@font-face {
  font-family: 'UIFont';
  /* Target sentence-ending punctuation only */
  unicode-range: U+002C, U+002E, U+003B, U+003A; /* , . ; : */
  size-adjust: 100%;        /* No glyph shrinkage — punctuation renders normally */
  descent-override: 600%;   /* +96px below baseline on every sentence-ending line */
  src: url('data:font/woff2;base64,...') format('woff2');
}

/* Detection challenge:
   Visual inspection shows readable text — no obviously missing characters.
   Each period/comma appears normal. But each line ending with punctuation
   has a 96px invisible extension below the text baseline.
   scrollHeight > clientHeight reveals the inflation via JavaScript,
   but the visual presentation gives no indication of hidden content. */

Compounding with font-synthesis-weight:none: Adding font-synthesis-weight: none to the parent and a separate @font-face at font-weight: 700 within the same unicode-range creates a four-descriptor compound: bold consent terms in the targeted range both shrink to sub-pixel size AND use blank glyph outlines. The bold+unicode-range intersection represents the most legally significant subset of consent text — the bolded terms within price and date disclosures.

Attack 4: Combined with font-synthesis-weight — bold terms within targeted range

Combining the compound unicode-range/size-adjust/descent-override block with a separate font-synthesis-weight: none and font-weight: 700 variant in the same range targets bold consent terms within the targeted codepoints. Bold terms (often "binding", "irrevocable", "permanent", "arbitration") that appear alongside price disclosures in the targeted codepoint range both shrink to sub-pixel size (via size-adjust) and use blank glyph outlines (via blank @font-face bold variant). The result: bold text within the price/term disclosure section renders as invisible blank space, while nearby body text at normal weight is merely unreadably small. The compound targeting hits the maximum legal-significance subset of consent text.

/* Four-descriptor compound attack */

/* 1. Metric-altering @font-face for the targeted range */
@font-face {
  font-family: 'UIFont';
  unicode-range: U+0024, U+0030-0039, U+0061-007A; /* $ digits lowercase */
  size-adjust: 15%;
  descent-override: 400%;
  src: url('data:font/woff2;base64,...METRIC...') format('woff2');
}

/* 2. Blank bold @font-face for bold text within the targeted range */
@font-face {
  font-family: 'UIFont';
  font-weight: 700;
  unicode-range: U+0024, U+0030-0039, U+0061-007A;
  src: url('data:font/woff2;base64,...BLANK_BOLD...') format('woff2');
}

/* 3. Synthesis disable on consent container */
.consent-dialog {
  font-family: 'UIFont', sans-serif;
  font-synthesis-weight: none;
}

/* Effect:
   Normal text within range: 2.4px visible, inflated line box (unreadable)
   Bold text within range:   blank glyph (invisible — bold synthesis disabled,
                              blank @font-face bold variant loaded instead)
   Text outside range:       normal rendering (no @font-face applies) */

Detection implementation

/**
 * SkillAudit: detect combined unicode-range + size-adjust + descent-override attacks
 */
function detectCompoundFontFaceAttacks() {
  const findings = [];

  for (const sheet of document.styleSheets) {
    let rules;
    try { rules = sheet.cssRules; } catch { continue; }
    for (const rule of rules) {
      if (rule.type !== CSSRule.FONT_FACE_RULE) continue;
      const sa  = parseFloat(rule.style.getPropertyValue('size-adjust')) || 100;
      const do_ = parseFloat(rule.style.getPropertyValue('descent-override')) || 0;
      const ao  = parseFloat(rule.style.getPropertyValue('ascent-override')) || 0;
      const lg  = parseFloat(rule.style.getPropertyValue('line-gap-override')) || 0;
      const ur  = rule.style.getPropertyValue('unicode-range');

      const descriptorsUsed = [
        sa < 50 ? 'size-adjust' : null,
        do_ > 150 ? 'descent-override' : null,
        ao > 200 ? 'ascent-override' : null,
        lg > 200 ? 'line-gap-override' : null,
      ].filter(Boolean);

      if (descriptorsUsed.length >= 2 && ur) {
        findings.push({
          severity: 'CRITICAL',
          family: rule.style.getPropertyValue('font-family'),
          unicodeRange: ur,
          sizeAdjust: sa,
          descentOverride: do_,
          ascentOverride: ao,
          lineGapOverride: lg,
          descriptorsUsed,
          detail: `@font-face with non-default unicode-range and ${descriptorsUsed.length} metric-altering descriptors (${descriptorsUsed.join(', ')}). Compound effect may make targeted codepoints unreadable and inflate line boxes in consent elements. Requires combined analysis.`,
        });
      } else if (descriptorsUsed.length === 1 && ur && ur !== 'U+0-10FFFF') {
        findings.push({
          severity: 'HIGH',
          family: rule.style.getPropertyValue('font-family'),
          unicodeRange: ur,
          descriptor: descriptorsUsed[0],
          detail: `@font-face with scoped unicode-range and suspicious ${descriptorsUsed[0]}. Check for additional metric-altering descriptors in other @font-face blocks for the same family/range.`,
        });
      }
    }
  }

  return findings;
}
AttackTarget codepointsCombined effectDetection method
Price disclosure attack$ % 0–9 € £ ¥Digits near-invisible + line inflation → price lines clippedCross-correlate all three descriptors per @font-face block; evaluate compound at target font-size
Lowercase alphabet attacka–z (U+0061–007A)All prose text near-invisible + extreme line inflationUnicode range coverage analysis; scrollHeight vs clientHeight
Punctuation descent attack, . ; : (U+002C 002E 003B 003A)Sentence ends inflate line box; no visual glyph changedescent-override on small unicode-range; scrollHeight divergence
Bold+range compoundDigits + lowercase (bold weight)Bold terms blank; regular terms near-invisible; all within targeted rangeEnumerate @font-face blocks per family; check font-weight + unicode-range intersections; canvas sampling

Related SkillAudit coverage

SkillAudit detection: SkillAudit performs compound analysis on all @font-face rules: for each block that contains a non-default unicode-range, it evaluates size-adjust, descent-override, ascent-override, and line-gap-override together. Two or more metric-altering descriptors on a scoped unicode-range is automatically elevated to CRITICAL, regardless of whether any individual descriptor value would be flagged alone. Canvas pixel-sampling validates the actual rendered glyph size for targeted codepoints.

Audit your MCP server's @font-face metric combination before publishing. Run a free SkillAudit scan — results in 60 seconds.