AntiDetect
Features Technology Pricing Who's It For
Docs Blog FAQ
Download

Your Font List Is an Operating System Confession

No API tells a website which fonts you have installed. It measures them instead — by drawing text and comparing widths, a few hundred times, in under fifty milliseconds. The result carries roughly fourteen bits, and it will contradict a spoofed user agent without hesitation.

Measurement, not enumeration

There has never been a web API that returns a list of installed fonts. There has never needed to be one, because measuring is almost as good and requires no permission at all.

The technique is the same one used since roughly 2007. Render a string in a known fallback font and record its width. Then render the same string asking for the candidate font first, with the same fallback behind it. If the width changes, the candidate exists and was used. If it does not, the candidate is missing and the fallback drew it again.

const span = document.createElement('span');
span.style.cssText = 'position:absolute;left:-9999px;font-size:72px';
span.textContent = 'mmmmmmmmmmlli';

span.style.fontFamily = 'monospace';
document.body.appendChild(span);
const base = span.offsetWidth;

span.style.fontFamily = '"Segoe UI", monospace';
const installed = span.offsetWidth !== base;

The test string is not arbitrary. mmmmmmmmmmlli mixes the widest and narrowest glyphs in most Latin faces, maximising the width difference between any two fonts. At 72 pixels the difference is unmissable.

Run this across a list of two or three hundred candidates and you have the font profile. It costs a few tens of milliseconds and leaves no trace in any permission prompt.

The same test string rendered in two typefaces with their widths measured underneath
The entire technique. Render the string in the candidate font with a known fallback behind it: if the width moves, the font is installed.

Why fonts identify the operating system

Font sets are not random. They are shipped with the OS, and each platform ships a distinctive set.

FontShips withWhat its presence means
Segoe UIWindows Vista and laterWindows, near-conclusively
Segoe UI VariableWindows 11Distinguishes 11 from 10
Helvetica NeuemacOSApple platform
Ubuntu, Liberation SansCommon Linux distributionsLinux
Calibri, CambriaMicrosoft OfficeOffice installed; weak Windows signal
Arial, Times New RomanAlmost everywhereNothing

This is the check that catches a spoofed user agent for free. A profile claiming Windows 11 that cannot render Segoe UI Variable has contradicted itself in one offsetWidth comparison — no external data, no reputation database, no machine learning.

The reverse mistake is just as common and harder to spot: a profile claiming macOS on a Windows host, where the host's Segoe UI is still installed and measurable. The user agent says Mac; the fonts say Windows; the fonts win.

How much it is worth

The EFF's Panopticlick work put font lists at roughly 13.9 bits of identifying information — enough on its own to narrow a browser to one in about fifteen thousand. That is among the highest single-signal figures in the fingerprinting literature, above screen resolution and well above the user agent string.

Two caveats keep it honest. First, that figure is from a self-selected population of privacy-interested users and overstates entropy for the general web. Second, it has been falling: Windows 11, recent macOS and mainstream Linux distributions ship increasingly similar default sets, and web fonts have removed most of the reason for anyone to install extras.

What survives the decline is the part that matters here. Even if the font list no longer uniquely identifies a machine, it still identifies the platform — and platform identification is what breaks an inconsistent profile.

The other ways to ask

Width measurement is the classic method, not the only one. An implementation that hooks only offsetWidth will miss these.

Canvas measureText

ctx.measureText() returns a TextMetrics object with sub-pixel advance width plus, in modern browsers, actual bounding box ascent and descent. It is more precise than element measurement and it lives in the canvas API, so it can be run inside an OffscreenCanvas in a worker where page-level hooks do not reach.

Pure CSS

Font detection can be done without JavaScript at all, using @font-face with unicode-range and a background image request that only fires when a particular fallback is used. No script to hook, no API to override.

Local Font Access

Chrome 103 shipped window.queryLocalFonts(), which returns the full local font list directly — behind a user permission prompt. In practice fingerprinting scripts do not use it, precisely because it prompts. It is worth knowing about mainly because a browser that claims Chrome 147 should have the API present, and a profile that removed it has created a new inconsistency.

Getting it right

Two approaches work, and one very common one does not.

What does not work: returning an empty or minimal list. A browser with five fonts is more remarkable than one with two hundred. It is the same error as randomising canvas — you have replaced a common value with a rare one and called it privacy.

Approach one: ship the right set. The profile claims Windows 11, so the font list is the Windows 11 default set, present and measurable. This is only fully achievable when the font stack itself is under control — the browser has to resolve font requests against a declared set rather than against whatever the host has installed. That is an engine-level concern, which is why it is one of the harder things to retrofit.

Approach two: do not claim a platform you are not on. Run Windows profiles on Windows and Mac profiles on Mac. Unglamorous, entirely effective, and free. If you have a choice of host, this is the cheapest correct answer available.

Whichever route you take, the constraint from the consistency article applies: the font set has to agree with the user agent, with the WebGL renderer and with the platform string. Fonts are simply the cheapest of the four to check, which is why they are checked first.

Checking your own

The quickest test is a single probe against the OS you claim. Paste this into the console of a profile and read the answer:

function has(f) {
  const s = document.createElement('span');
  s.style.cssText = 'position:absolute;left:-9999px;font-size:72px';
  s.textContent = 'mmmmmmmmmmlli';
  document.body.appendChild(s);
  s.style.fontFamily = 'monospace';       const base = s.offsetWidth;
  s.style.fontFamily = `"${f}", monospace`; const w = s.offsetWidth;
  s.remove();
  return w !== base;
}
console.log({
  windows: has('Segoe UI'),
  win11:   has('Segoe UI Variable'),
  macos:   has('Helvetica Neue'),
  linux:   has('Ubuntu')
});

Exactly one of those should be true, and it should be the one matching your user agent. Two true values means your profile is wearing two operating systems at once. For a fuller picture, browserleaks.com/fonts and CreepJS both enumerate the complete set and, in CreepJS's case, will tell you outright when the list disagrees with the platform you claim.

Font sets that match the machine

P8 ships each profile with the default font set for the OS version it claims — not the host’s fonts with a different user agent on top.

Start for Free
All articles Next: Why Ad Accounts Actually Get Banned