Skip to content
Fingerprinting

Locale, time and glyph tells: small probes that reveal the host

A flag emoji, five historical dates and a few Intl strings say more about the machine underneath than the User-Agent does.

Pim
Pim· Clearcote Research
8 min read

Key takeaways

  • Windows draws a flag emoji as two letters, while macOS, iOS, Android and Linux with a colour emoji font draw a flag, so one canvas readback of a flag separates Windows from everything else.
  • Parsing the same day as YYYY-12-01 (UTC by spec) and 12/1/YYYY (local time) gives the timezone's offset in that year; historical years such as 1937 and 1945 expose whether a real timezone database stands behind the claimed zone.
  • Intl output such as compact numbers, list formats and display names depends on the language and the engine's ICU data, so it has to agree with navigator.languages, Accept-Language and the claimed browser version.

Why small probes work

The large fingerprint surfaces, canvas, WebGL and audio, get most of the attention and most of the spoofing. The probes in this article are small: one emoji, five dates, a handful of formatted strings. They are cheap to run, they produce few bits each, and they are rarely covered by spoofing tools, because each reads a value that comes from a part of the system nobody thinks of as identifying: the emoji font, the timezone database, the ICU locale data, the keyboard driver.

Each is also tied to something the session claims elsewhere. That is what makes them useful: not the value, but whether it fits the operating system, language and location the rest of the fingerprint states. All of them appear in a deployed challenge widget; see inside a challenge widget for the full inventory.

The flag emoji

A country flag is not one character. It is two regional indicator symbols, for example 🇺 and 🇸, which a font may render as one flag glyph. Apple's emoji font does, Google's Noto Color Emoji does, and so does every Android device. The Windows emoji font does not: it draws the two letters U and S side by side.

A probe draws the US flag to a small canvas with a sans-serif font and reads the pixels back. The colours present tell flag from letters without any reference image. A session that claims Windows and draws a flag is running on something else, usually a Linux host with a colour emoji font installed; a session that claims macOS and draws letters has the same problem in reverse. This is the same class of tell as the font checks in the font stack under the user agent: rendering happens on the host, whatever the User-Agent says.

Five dates and the timezone database

ECMAScript parses a date-only ISO string such as 1937-12-01 as UTC and a string such as 12/1/1937 as local time. Parse both and subtract, and the difference is the local zone's offset from UTC on that day in that year. A deployed probe does this for the years 999, 1060, 1937, 1945 and 1989 and also reads the zone name from Intl.DateTimeFormat().resolvedOptions().timeZone.

Today's offset is easy to fake: override getTimezoneOffset and the Intl zone name and most checks are satisfied. Historical offsets are not. Before standard time, most places used local mean time, an offset with minutes and seconds set by longitude. Wartime years carry offsets that applied for a few months. Only a real timezone database knows them, and the browser's comes from ICU and the operating system, not from a JavaScript override. If the zone name says Europe/Amsterdam and the 1937 offset is not Amsterdam's, the zone was set in JavaScript and not in the engine.

The same probe exposes an engine difference: the major engines disagree on how to parse some three-digit years, so the result of the year-999 parse depends on the engine as well as the zone. Even the date check has to fit the claimed browser.

Intl strings and the keyboard layout

Intl output is generated from locale data shipped with the engine. A probe formats a few deliberately unusual things: a date with the month and the long timezone name, the display name of Esperanto as spoken in Ukraine, a list joined with or, and a large number in compact notation. In an English browser with UTC those come out as December at Coordinated Universal Time, Esperanto (Ukraine), a, b, or c and 21,000 trillion. In a Dutch browser in Amsterdam they become december om Midden-Europese standaardtijd, Esperanto (Oekraïne), a, b of c and 21.000 biljoen.

Those strings have to agree with navigator.languages, with the Accept-Language header the server saw, and with the timezone from the dates above. They also carry the version of the locale data, which changes between browser releases, so they have to fit the claimed version too. The timezone and IP mismatch answer covers the network side of the same question.

Chromium on desktop adds navigator.keyboard.getLayoutMap(), which maps physical keys to the characters the active layout produces. The layout does not have to match the language exactly; many people outside the US type on a US layout. But it has to be plausible for it, and keys such as IntlBackslash differ between layouts and platforms.

Fifty-two fonts, three platforms

The font probe in the same widget does not test a long generic list. It tests 52 names chosen to separate platforms: Windows faces (Cambria Math, Segoe UI Emoji, Segoe Fluent Icons, Bahnschrift, HoloLens MDL2 Assets), Apple faces (Apple Symbols, Skia, Geneva, PingFang HK Light, Galvji), Linux desktop faces (Ubuntu, Cantarell, Adwaita Sans, DejaVu Sans, Liberation, WenQuanYi Zen Hei) and Office or ChromeOS faces (Arimo, Tinos, Cousine, Caladea, Carlito). It checks each with FontFace and local(), with document.fonts.check and by measuring text.

One name alone is weak evidence. An iPhone reports Apple Symbols, Galvji and Carlito, so Carlito is not proof of Linux. What separates platforms is the combination: Segoe Fluent Icons next to Cantarell does not occur on any real machine. A persona's font list has to be a set that exists, not a set that merely includes the expected names.

The through-line

Each probe here reads one small value from the layer below the browser, the emoji font, the tz database, ICU or the keyboard driver, and the check is always the same: does it fit what the session claims? That is why these tells are hard to answer from JavaScript and why controlling them belongs in the engine and the host, where Clearcote's personas set them together with the platform, as described in the coherence breakdown.

For the overall map of signals and how they must agree, read anatomy of a browser fingerprint. The fingerprint test runs the timezone, language and font comparisons against your own browser.

#fingerprinting#coherence#fonts#reference

Clearcote puts this into practice

An open-source Chromium with fingerprint control compiled into the engine. A drop-in for Playwright & Puppeteer.

  • 222engine patches
  • 34,478lines added
  • 330Chromium files changed
  • 354checks per build

Engine numbers are for the Pro build; the free open build is a smaller, public patch set.

Free for one browser with GitHub. No card.