cdpfleet Docs GitHub Dashboard

Cases / #52 · 2026-10-16 · Medium

Installed fonts as a fingerprint: what a width probe sees on each Camoufox persona

The classic font probe — 46 families measured against the generic fonts — on Camoufox's Windows, macOS and Linux personas, with extra `fonts` and with `custom_fonts_only`. The list follows the OS and is identical every launch; `fonts` can only add what the server has; and one face is always invisible.

Camoufox

Run on production on 2026-10-16: ✓ Node.js ✓ Python ✓ Java ✓ C# ✓ Go

The problem

Which fonts a machine has is one of the oldest fingerprinting signals: a script sets a test string in each candidate family and measures it. A Windows user agent with macOS fonts, or a machine with three fonts in total, stands out. Camoufox builds the font list from its OS persona and lets you add to it or replace it. What does a probe actually see, is it the same every launch, and what do fonts and custom_fonts_only change?

What we used, and why

WhatWhy
A width probe over 46 familiesEach family is set with monospace, serif and sans-serif as fallback; if any measurement differs from the generic family alone, the family counts as installed. This is how common fingerprinting libraries do it.
os: "windows" | "macos" | "linux"The persona decides which families the page can see.
fonts: [...]Families exposed in addition to the persona's list.
custom_fonts_only: trueExpose only the families in fonts.
A hash of the detected listOne value per launch that a site could store and compare.

How it works

  1. Launch Camoufox six times one after another: Windows twice, macOS, Linux, Windows with three extra fonts, and Windows with a custom list only.
  2. In each, run the width probe over the same 46 candidates and record the detected families, their hash, and whether the three generic families measure the same.

The code

The same program in five languages (also on GitHub, with the raw output). Set these environment variables first:

// npm install [email protected]
// env: CDPFLEET_API_KEY, PROXY_URL
import { createHash } from 'node:crypto';
import { firefox } from 'playwright';

const KEY = process.env.CDPFLEET_API_KEY;

// The families a font-fingerprinting script typically tries: Windows, macOS, Linux and popular extras.
const CANDIDATES = [
  'Arial', 'Arial Black', 'Calibri', 'Cambria', 'Candara', 'Comic Sans MS', 'Consolas', 'Constantia', 'Corbel', 'Courier New',
  'Georgia', 'Impact', 'Lucida Console', 'Lucida Sans Unicode', 'Microsoft Sans Serif', 'Palatino Linotype', 'Segoe UI', 'Tahoma',
  'Times New Roman', 'Trebuchet MS', 'Verdana', 'American Typewriter', 'Avenir', 'Avenir Next', 'Futura', 'Geneva', 'Gill Sans',
  'Helvetica', 'Helvetica Neue', 'Lucida Grande', 'Menlo', 'Monaco', 'Optima', 'SF Pro', 'DejaVu Sans', 'DejaVu Serif',
  'Liberation Sans', 'Liberation Mono', 'Noto Sans', 'Ubuntu', 'Cantarell', 'Roboto', 'Open Sans', 'Fira Code', 'JetBrains Mono',
  'Source Code Pro',
];

// The classic width probe: a family is "installed" if text set in it, with a generic fallback,
// measures differently from the generic family alone.
const PROBE = `(fonts) => {
  const span = document.createElement('span');
  span.textContent = 'mmmmmmmmmmlli10OWQ@#';
  span.style.cssText = 'position:absolute;left:-9999px;font-size:72px';
  document.body.appendChild(span);
  const size = (family) => { span.style.fontFamily = family; return span.offsetWidth + 'x' + span.offsetHeight; };
  const generics = ['monospace', 'serif', 'sans-serif'];
  const base = {};
  for (const g of generics) base[g] = size(g);
  const found = fonts.filter((f) => generics.some((g) => size('"' + f + '", ' + g) !== base[g]));
  span.remove();
  return { platform: navigator.platform, found, generic_sizes: generics.map((g) => base[g]) };
}`;

const SETUPS = [
  { setup: 'os: windows (launch 1)', body: { os: 'windows' } },
  { setup: 'os: windows (launch 2)', body: { os: 'windows' } },
  { setup: 'os: macos', body: { os: 'macos' } },
  { setup: 'os: linux', body: { os: 'linux' } },
  { setup: 'windows + fonts', body: { os: 'windows', fonts: ['Menlo', 'Helvetica Neue', 'Fira Code'] } },
  { setup: 'windows + fonts + custom_fonts_only', body: { os: 'windows', fonts: ['Calibri', 'Consolas', 'Segoe UI', 'Fira Code'], custom_fonts_only: true } },
];

// 503 means the fleet is momentarily busy: wait Retry-After and try again.
async function launch(body) {
  for (let attempt = 1; ; attempt++) {
    const res = await fetch('https://starter.cdpfleet.com/camoufox/session', {
      method: 'POST',
      headers: { 'x-api-key': KEY, 'content-type': 'application/json' },
      body: JSON.stringify(body),
    });
    if (res.ok) return res.json();
    if (res.status !== 503 || attempt === 5) throw new Error(`launch ${res.status} ${await res.text()}`);
    await new Promise((r) => setTimeout(r, (Number(res.headers.get('retry-after')) || 5) * 1000));
  }
}

// Proxies drop a tunnel now and then: up to two retries.
async function go(page, url) {
  for (let attempt = 1; ; attempt++) {
    try {
      return await page.goto(url, { timeout: 60000 });
    } catch (err) {
      if (attempt === 3) throw err;
      await new Promise((r) => setTimeout(r, 2000));
    }
  }
}

const results = [];
for (const s of SETUPS) {
  const session = await launch({ ...s.body, proxy: process.env.PROXY_URL, headless: true, record: false });
  const browser = await firefox.connect(session.wsUrl, { headers: { 'x-api-key': KEY } });
  try {
    const page = await (await browser.newContext()).newPage();
    await go(page, 'https://httpbin.org/html');
    const r = await page.evaluate(`(${PROBE})(${JSON.stringify(CANDIDATES)})`);
    results.push({
      setup: s.setup,
      platform: r.platform,
      detected: r.found.length,
      font_hash: createHash('sha256').update(r.found.join(',')).digest('hex').slice(0, 10),
      generics_identical: new Set(r.generic_sizes).size === 1,
      fonts: r.found,
    });
  } finally {
    await browser.close();
  }
}
console.log(JSON.stringify(results, null, 2));

What we got

Setupnavigator.platformDetected (of 46)List hashGenerics identicalFamilies found
os: windows (launch 1)Win32208ae0b1fd1dyesArial Black, Calibri, Cambria, Candara, Comic Sans MS, Consolas, Constantia, Corbel, Courier New, Georgia, Impact, Lucida Console, Lucida Sans Unicode, Microsoft Sans Serif, Palatino Linotype, Segoe UI, Tahoma, Times New Roman, Trebuchet MS, Verdana
os: windows (launch 2)Win32208ae0b1fd1dyesArial Black, Calibri, Cambria, Candara, Comic Sans MS, Consolas, Constantia, Corbel, Courier New, Georgia, Impact, Lucida Console, Lucida Sans Unicode, Microsoft Sans Serif, Palatino Linotype, Segoe UI, Tahoma, Times New Roman, Trebuchet MS, Verdana
os: macosMacIntel2323e513dd7ayesArial, Arial Black, Comic Sans MS, Courier New, Georgia, Impact, Microsoft Sans Serif, Tahoma, Times New Roman, Trebuchet MS, Verdana, American Typewriter, Avenir, Avenir Next, Futura, Geneva, Gill Sans, Helvetica, Helvetica Neue, Lucida Grande, Menlo, Monaco, Optima
os: linuxLinux x86_6435bebd342f7yesArial, Courier New, Times New Roman
windows + fontsWin3222049bd59a6dyesArial Black, Calibri, Cambria, Candara, Comic Sans MS, Consolas, Constantia, Corbel, Courier New, Georgia, Impact, Lucida Console, Lucida Sans Unicode, Microsoft Sans Serif, Palatino Linotype, Segoe UI, Tahoma, Times New Roman, Trebuchet MS, Verdana, Helvetica Neue, Menlo
windows + fonts + custom_fonts_onlyWin3220fc1fd0a15yesConsolas, Segoe UI

From the Node.js run on 2026-10-16. IP addresses are replaced with placeholders (203.0.113.x); equal addresses stay equal. The other languages produced the same findings.

Raw output (Node.js)
[
  {
    "setup": "os: windows (launch 1)",
    "platform": "Win32",
    "detected": 20,
    "font_hash": "8ae0b1fd1d",
    "generics_identical": true,
    "fonts": [
      "Arial Black",
      "Calibri",
      "Cambria",
      "Candara",
      "Comic Sans MS",
      "Consolas",
      "Constantia",
      "Corbel",
      "Courier New",
      "Georgia",
      "Impact",
      "Lucida Console",
      "Lucida Sans Unicode",
      "Microsoft Sans Serif",
      "Palatino Linotype",
      "Segoe UI",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana"
    ]
  },
  {
    "setup": "os: windows (launch 2)",
    "platform": "Win32",
    "detected": 20,
    "font_hash": "8ae0b1fd1d",
    "generics_identical": true,
    "fonts": [
      "Arial Black",
      "Calibri",
      "Cambria",
      "Candara",
      "Comic Sans MS",
      "Consolas",
      "Constantia",
      "Corbel",
      "Courier New",
      "Georgia",
      "Impact",
      "Lucida Console",
      "Lucida Sans Unicode",
      "Microsoft Sans Serif",
      "Palatino Linotype",
      "Segoe UI",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana"
    ]
  },
  {
    "setup": "os: macos",
    "platform": "MacIntel",
    "detected": 23,
    "font_hash": "23e513dd7a",
    "generics_identical": true,
    "fonts": [
      "Arial",
      "Arial Black",
      "Comic Sans MS",
      "Courier New",
      "Georgia",
      "Impact",
      "Microsoft Sans Serif",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana",
      "American Typewriter",
      "Avenir",
      "Avenir Next",
      "Futura",
      "Geneva",
      "Gill Sans",
      "Helvetica",
      "Helvetica Neue",
      "Lucida Grande",
      "Menlo",
      "Monaco",
      "Optima"
    ]
  },
  {
    "setup": "os: linux",
    "platform": "Linux x86_64",
    "detected": 3,
    "font_hash": "5bebd342f7",
    "generics_identical": true,
    "fonts": [
      "Arial",
      "Courier New",
      "Times New Roman"
    ]
  },
  {
    "setup": "windows + fonts",
    "platform": "Win32",
    "detected": 22,
    "font_hash": "049bd59a6d",
    "generics_identical": true,
    "fonts": [
      "Arial Black",
      "Calibri",
      "Cambria",
      "Candara",
      "Comic Sans MS",
      "Consolas",
      "Constantia",
      "Corbel",
      "Courier New",
      "Georgia",
      "Impact",
      "Lucida Console",
      "Lucida Sans Unicode",
      "Microsoft Sans Serif",
      "Palatino Linotype",
      "Segoe UI",
      "Tahoma",
      "Times New Roman",
      "Trebuchet MS",
      "Verdana",
      "Helvetica Neue",
      "Menlo"
    ]
  },
  {
    "setup": "windows + fonts + custom_fonts_only",
    "platform": "Win32",
    "detected": 2,
    "font_hash": "0fc1fd0a15",
    "generics_identical": true,
    "fonts": [
      "Consolas",
      "Segoe UI"
    ]
  }
]

Takeaways