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. */
| Attack | Prerequisite | What it enables | Severity |
|---|---|---|---|
| Active font family identification via ex-height | CSS + JS + Chrome 127+ or Firefox 3+ | Identifies which font is active on host elements; discriminates system fonts by OS | HIGH |
| Font availability timing oracle via fallback reflow | CSS + JS + ResizeObserver | Determines whether a named font is installed, blocked by CSP, or loaded from cache | HIGH |
| OS identity via cap-height metric differences | CSS + JS + Chrome 127+ / Safari 17+ | Fingerprints host OS from same-named font metric variations; ~80% accuracy without UA string | MEDIUM |
| Developer tool detection via ch-width monospace advance | CSS + JS + requestAnimationFrame | Enumerates installed monospace fonts, revealing IDE ecosystem and developer tool preferences | MEDIUM |
Defences
- CSP
style-srcblocks injected stylesheets from settingfont-size-adjuston probe elements. - Resist font fingerprinting. Firefox's enhanced tracking protection and Tor Browser fingerprint-resist mode normalize font metrics —
font-size-adjustprobes return identical values regardless of the actual installed font. - Use web fonts rather than system fonts for security-sensitive UI. A consistently loaded web font has a single known metric ratio across all platforms, eliminating the OS-identity side channel from
cap-height. - Block
ResizeObserverandIntersectionObserverin sandboxed contexts. MCP tool execution environments that remove ResizeObserver from the global scope neutralize the font-availability timing oracle. - SkillAudit flags:
font-size-adjustapplied to elements the MCP server creates (position:fixed; top:-9999pxpattern);getComputedStyle(el).fontSizereads afterfont-size-adjustinjection;ResizeObservercallbacks on off-screen probe elements.
SkillAudit findings for this attack surface
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.