Open Bug 2044813 Opened 3 months ago Updated 3 months ago

Canonicalize Intl.DateTimeFormat().resolvedOptions().timeZone

Categories

(Core :: Privacy: Anti-Tracking, enhancement)

enhancement

Tracking

()

People

(Reporter: tjr, Unassigned)

References

(Blocks 1 open bug)

Details

Reduce intl_timezone fingerprint surface by canonicalizing Intl.DateTimeFormat().resolvedOptions().timeZone to one of the 88 IANA zonenow.tab representatives instead of returning the user's most-specific IANA city name.

Background — what gets exposed today

Every web page can call:

new Intl.DateTimeFormat().resolvedOptions().timeZone
// Returns e.g. "Europe/Vienna", "America/Detroit", "Asia/Kolkata"

Firefox returns whatever IANA name ICU resolves via ucal_getDefaultTimeZone(). On Windows this comes from the windowsZones.xml CLDR mapping of Windows registry zone names to IANA names; on Linux, macOS, Android, it comes from the OS's bundled tzdata via /etc/localtime / NSTimeZone / Calendar.getInstance().

The API is on by default, no permission prompt, available to any cross-origin script.

The problem

The set of returnable names is much larger than the set of distinct future clock behaviors. As an example, every one of these IANA names returns a zone with literally identical future clock behavior to every other:

Europe/Berlin, Europe/Paris, Europe/Vienna, Europe/Madrid, Europe/Amsterdam,
Europe/Zurich, Europe/Prague, Europe/Warsaw, Europe/Rome, Europe/Belgrade,
Europe/Brussels, Europe/Budapest, Europe/Copenhagen, Europe/Luxembourg,
Europe/Oslo, Europe/Stockholm, Europe/Tirane, Europe/Vaduz, Europe/Zagreb,
Europe/Sarajevo, Europe/Skopje, Europe/Bratislava, Europe/Ljubljana, ...

A script reading resolvedOptions().timeZone for a Vienna user learns "user is in Vienna" without learning anything about the user's clock that it wouldn't have learned from any of those other names. The geographic granularity is essentially gratuitous fingerprinting.

Quantified across the deduplicated population: H(intl_timezone) = 5.69 bits across 446 distinct names. For comparison:

  • H(functional_class) ≈ 4.0 bits (collapsing to the 88 zonenow.tab representatives)
  • H(summer_offset) ≈ 3.27 bits (collapsing to ~24 UTC offsets only)

The IANA-name layer carries ~1.7 bits more than the underlying clock behavior.

Proposed change

Replace the timezone identifier exposed via Intl.DateTimeFormat().resolvedOptions().timeZone (and the corresponding telemetry collection in nsUserCharacteristics.cpp::PopulateTimeZone()) with the canonical representative from IANA's zonenow.tab file.

What zonenow.tab is. A file shipped in current IANA tzdata releases (introduced 2024-12-18 by Paul Eggert). Each row represents one timezone class where, by IANA's definition, civil clocks are guaranteed to agree at all times from now on. The file is explicitly curated as "the minimum set a UA can use without misrepresenting any user's future clock behavior." In tzdata 2026 on a current Linux system it contains 88 representatives:

$ grep -c '^XX ' /usr/share/zoneinfo/zonenow.tab
88

Every IANA zone in zone1970.tab and the backward aliases maps to exactly one of these 88. The mapping is generated at build time from the bundled IANA tzdata; no manual table maintenance is required.

Example canonicalizations:

Europe/Vienna       → Europe/Paris       (EU CEST class representative)
Europe/Madrid       → Europe/Paris
Europe/Berlin       → Europe/Paris
America/Detroit     → America/New_York   (US Eastern DST class)
America/Toronto     → America/New_York
America/Indianapolis → America/New_York
Asia/Calcutta       → Asia/Kolkata       (India class)
Asia/Kolkata        → Asia/Kolkata
Europe/Kiev         → Europe/Kyiv
Europe/Kyiv         → Europe/Kyiv
Australia/Currie    → Australia/Hobart   (Tasmania class)

Zones with unique future clock behavior — Newfoundland half-hour offset, Lord Howe half-hour DST shift, Chatham +12:45/+13:45, Iran +03:30, Nepal +05:45, Marquesas -09:30, Eucla +08:45, Antarctica/Troll's +00/+02 station-specific DST, Israel / Lebanon / Egypt / Palestine independent DST schedules, Cuba DST, Chile DST, Morocco Ramadan-adjusted DST — stay as their own bucket, because IANA considers them behaviorally distinct from any other zone going forward.

Where the substitution happens. The canonicalization is applied at the ICU TimeZone boundary — the JS engine's default TimeZone object is replaced with the canonical zone, not the user's specific city. As a result:

  • Intl.DateTimeFormat().resolvedOptions().timeZone returns the canonical name.
  • Date.prototype.getTimezoneOffset() returns the canonical zone's offset for any given moment.
  • new Date() arithmetic uses the canonical zone's tzdata, including historical transitions.
  • Intl.DateTimeFormat().format(d) renders times in the canonical zone.
  • Intl.supportedValuesOf('timeZone') returns the 88 canonical representatives only.
  • The Glean telemetry collection in nsUserCharacteristics.cpp:1094-1108 inherits the canonicalization automatically because it calls the same ICU path.

The OS-native system clock is not affected — the user's calendar app, file timestamps, and other native-API time displays continue to use the user's real zone. Only what Firefox exposes via JS APIs changes.

Substituting at the ICU layer (rather than only rewriting the string returned by resolvedOptions().timeZone) is what makes the canonicalization actually defend against fingerprinting. A pure string rewrite at the API boundary would be trivially bypassable: a script could probe Date.prototype.getTimezoneOffset() at historical dates where the user's actual zone diverges from the canonical representative — e.g., 1940 for Vienna vs Paris, 1972 for Detroit vs New York, 1942 for Madrid vs Paris — and re-derive the user's true zone from a handful of probes. Substituting at the ICU layer closes that bypass: every JS-visible date/time answer is consistent with the canonical zone.

The compat surface is bounded. For any date within the user's zonenow.tab class — which by IANA's definition includes every moment from the canonicalization-effective date forward — wall-clock times rendered through Firefox match the user's actual local time exactly, because all members of the class have agreed on clocks since some past date and will continue to going forward. Divergence shows up only in distant-historical dates (pre-1972 for some US zones, pre-1942 for Spain, pre-WWII for many European cities, etc.) where the underlying IANA backward aliases reflect real period-specific offset differences. Sites that need historical accuracy for a specific zone can opt in by passing it explicitly via Intl.DateTimeFormat({timeZone: 'Europe/Vienna'}).

Spec compliance

ECMA-402 § "Intl.DateTimeFormat.prototype.resolvedOptions()" specifies timeZone as "the canonicalized name of the time zone." The TC39 spec language permits any valid IANA name representing the resolved zone. Returning Europe/Paris for a Vienna user is spec-compliant because Europe/Paris is a valid IANA name and represents the same forward-going zone (same future clock behavior).

The patch does not affect any other ECMA-402 method's signature.

Duplicate of this bug: 1978158

On top of our existing fingerprinting protections, this change reduces the percentage of unique users by these amounts.

FingerprintJS V2 — % unique Android Darwin Linux Windows
Baseline (P3) 51.70% 41.11% 68.47% 34.90%
+ Timezone canonicalize 49.79% 38.20% 64.68% 33.45%
Δ −1.91 −2.91 −3.79 −1.45

And the improvement in cohort score:

FPJS V2 — HHI Android Darwin Linux Windows
% improvement +6.34% +11.52% +7.89% +4.23%

While these aren't revolutionary improvements, this is a game of inches adding to feet, and this is a valuable reduction.

Duplicate of this bug: 1719738
You need to log in before you can comment on or make changes to this bug.