# Property enumeration order as a browser fingerprint

> Property enumeration order is a fingerprint that sorted diffs can't see. Compared unsorted, our engine differed from Chrome only in its 20 field-trial entries.

Source: https://mosaicbrowser.com/blog/enumeration-order-fingerprint

## Sorting the property list hid the question

Property enumeration order is a fingerprint that sorted diffs can't see. Compared unsorted, our engine differed from Chrome only in its 20 field-trial entries.

![20: Compared unsorted, twenty names were out of place, beside a screenshot: mosaic settings showing the installed Tesseract Chrome engine and its version](https://mosaicbrowser.com/covers/enumeration-order-fingerprint.webp)

Property enumeration order is a browser fingerprint in its own right: the order in which `Object.getOwnPropertyNames(window)`, or a `for...in` over `navigator`, lists its names. Every comparison we had written against Google Chrome sorted those lists first, and a sorted list cannot show order. When we compared them unsorted, the only differences in order were the twenty field-trial entries we already knew about. With those switched off, our engine matched branded Chrome name for name and position for position.

That took one family of suspects off the list while we were chasing IPHey's verdict. It also taught us something about our own instruments, which is the more useful half.

## Why the order of names says anything

JavaScript defines the order of an object's own string keys as the order they were created in. Integer-like keys come first, ascending; symbols come last. For a browser object that means the list reflects the order in which the engine installed each interface's attributes and methods. That order is fixed when the browser is built.

So a page can do this and hash the result:

```
Object.getOwnPropertyNames(window)
Object.getOwnPropertyNames(Navigator.prototype)
Object.getOwnPropertyNames(Document.prototype)

```

Two browsers with the same set of APIs installed in the same order give identical lists. A browser that enables one extra API puts a name somewhere in the middle, and every hash of the list changes. It moves without any single property's value changing, so a check that compares values one by one doesn't see it.

The comparison most of us write first is this one:

```
Object.getOwnPropertyNames(window).sort()

```

Sorting answers "which names exist". It discards "where each one sits". Two lists with the same names in a different order are equal after `.sort()`, so the diff reports nothing, and nothing looks a lot like a match.

Mosaic is a desktop app

Email yourself the link and open it on your computer.

[How it works: Machines that exist](https://mosaicbrowser.com/features/real-fingerprints)

## What we had been comparing

Our check `check:stock` launches the engine beside a stock browser of the same major and diffs what a page can read. It compared sorted name lists, and so did every one-off probe we wrote while working on IPHey's "inconsistent browser fingerprint" signal. Every one of them could find a missing name or an extra one. None could find a name in the wrong place.

The first unsorted run was against branded Google Chrome 153 on the same Mac, with our engine launched without `--disable-field-trial-config`. The order differences it found were exactly the twenty field-trial entries:

```
window                 ModelContext between ServiceWorkerContainer
                       and NotRestoredReasonDetails
Navigator.prototype    protectedAudience missing
Document.prototype     modelContext where browsingTopics should be

```

These are the experiments an unbranded Chromium runs from its compiled-in testing config at every launch, and Google Chrome doesn't. [Twenty APIs Chrome doesn't have](https://mosaicbrowser.com/blog/twenty-apis-chrome-lacks) has the whole list. What the unsorted diff added was the confirmation that they were the only thing out of order.

## The run with the switch on

With `--disable-field-trial-config`, which every real profile launch sends, the lists matched branded Chrome exactly, in order. That held across `window`, `Navigator`, `navigator`'s own properties, `Screen`, `Document`, `Element`, `Performance`, `CanvasRenderingContext2D`, `WebGL2RenderingContext`, `MediaDevices`, `window.chrome`, `CSSStyleDeclaration`, and `for...in` over both `navigator` and `window`.

And IPHey still scored the profile MX 90 where Chrome scored 100.

At the time that looked conclusive. Values, descriptors, names and now order all matched, so we wrote that the signal was not in the JavaScript surface at all and turned to the layer below. The TLS ClientHello and the HTTP/2 preamble turned out identical too, each about a minute's probe with no rebuild.

## What we took it to rule out

We took it as ruling out a patch bisect. A bisect costs a rebuild per step. Our patches act on surfaces a page reads, the surfaces we had compared now matched, so stepping through the series for a difference no page could see seemed pointless.

That reasoning was only as good as the list of surfaces behind it. The speech voice list had been read, compared and excused, on the grounds that it loads asynchronously. The probe read it before it had populated, got 0 in both browsers, and the excuse explained the 0. Branded Chrome lists 199 voices and any unbranded build 180, because 19 Google voices ship only with Chrome's branding ([the voice list story](https://mosaicbrowser.com/blog/chromium-speech-voices) has the numbers). The other half of IPHey's verdict was a Widevine device certificate sent in the clear, from a module our own series bundles in patch `0134`, so a bisect would have found that half. It is in [the (whale) signal write-up](https://mosaicbrowser.com/blog/iphey-whale-signal).

So "the JavaScript surface matches" was true for the parts our probes actually read, and one of the parts we had excused was the one that mattered. The order result itself stands: in order, our names and Chrome's agree.

## How to compare enumeration order properly

1. Keep the unsorted list. Compare it as a sequence and report the first index where the two differ, plus the neighbours on each side.
2. Compare the sorted list as well. It tells you which names are missing or extra, so you can tell an order difference from a set difference.
3. Read both browsers in the same world. A list read in an isolated world differs from one read in the main world: we once nearly reported `window.chrome.app` as missing because one probe read each browser from a different world.
4. Use a browser people actually run as the control. Our baseline had been Chrome for Testing, which carries its own differences from Chrome.
5. Treat every exemption in the diff as a measurement you've decided not to make, and re-read its reason. A value that loads late needs a probe that waits for it.

Order is one input among the many that make up a [browser fingerprint](https://mosaicbrowser.com/glossary/browser-fingerprint), and in our case it came out clean. It still deserves a place in the comparison, because it is the one difference a sorted diff can never report.

## Questions

### What is property enumeration order?

It is the order in which JavaScript lists an object's own properties, for example from Object.getOwnPropertyNames or a for...in loop. For string keys the language defines it as the order the properties were created, so it reflects how the browser built the object.

### Can a website fingerprint the order of browser APIs?

Yes. A page can list the names on window, Navigator.prototype or Document.prototype and hash them in the order returned. A browser that adds or drops an API, or installs one in a different place, produces a different list without changing any single value.

### Why does sorting a property list hide differences?

Sorting keeps which names exist and throws away where each one sits. Two browsers with the same set of names in a different order compare equal after sorting, so a diff built that way cannot report an order difference at all.

### Did matching enumeration order fix the IPHey signal?

No. It removed one family of suspects. The signal came from somewhere else: a speech voice list that one of our probes had excused, and a Widevine device certificate.

## Related

[Twenty APIs Chrome doesn't have: Chromium's field trials](https://mosaicbrowser.com/blog/twenty-apis-chrome-lacks): Unbranded Chromium runs its field-trial testing config at every launch. Against Chrome 153 that was 20 API differences, and one switch moved them all.

[199 voices: how a speech list tells Chromium from Chrome](https://mosaicbrowser.com/blog/chromium-speech-voices): speechSynthesis.getVoices() lists 199 voices in Google Chrome on our Mac and 180 in any unbranded Chromium. A page reads the difference in one call.

[IPHey's "(whale)" signal, and what it was really about](https://mosaicbrowser.com/blog/iphey-whale-signal): IPHey's inconsistent browser fingerprint (whale) looked like a version problem. We followed it through branding and JavaScript to a Widevine certificate.

[Browser fingerprint: what a site can read about your machine](https://mosaicbrowser.com/glossary/browser-fingerprint): A browser fingerprint is the set of values a site reads about your browser and computer, which together recognize you without cookies. What goes into one.

[Same IPHey score, opposite reasons](https://mosaicbrowser.com/blog/iphey-same-score-different-signal): IPHey Unreliable on a normal Chrome is usually the exit. Our profile and a stock Chrome both got MX 90, for a browser signal and a location signal.







## Mosaic, on your own computer

The browser this blog is about: its own Chromium, fingerprints from real machines and a proxy exit per profile. Free holds 3 profiles. Pro is $49 a month for any number.

Mosaic is a desktop app

Email yourself the link and open it on your computer.
