How anti-fingerprinting extensions tend to make fingerprinting easier

Do you have a privacy protection extension installed in your browser? There are so many around, and every security vendor is promoting their own. Typically, these will provide a feature called “anti-fingerprinting” or “fingerprint protection” which is supposed to make you less identifiable on the web. What you won’t notice: this feature is almost universally flawed, potentially allowing even better fingerprinting.

Pig disguised as a bird but still clearly recognizable
Image credits: OpenClipart

I’ve seen a number of extensions misimplement this functionality, yet I rarely bother to write a report. The effort to fully explain the problem is considerable. On the other hand, it is obvious that for most vendors privacy protection is merely a check that they can put on their feature list. Quality does not matter because no user will be able to tell whether their solution actually worked. With minimal resources available, my issue report is unlikely to cause a meaningful action.

That’s why I decided to explain the issues in a blog post, a typical extension will have at least three out of four. Next time I run across a browser extension suffering from all the same flaws I can send them a link to this post. And maybe some vendors will resolve the issues then. Or, even better, not even make these mistakes in the first place.

How fingerprinting works

When you browse the web, you aren’t merely interacting with the website you are visiting but also with numerous third parties. Many of these have a huge interest in recognizing you reliably across different websites, advertisers for example want to “personalize” your ads. The traditional approach is storing a cookie in your browser which contains your unique identifier. However, modern browsers have a highly recommendable setting to clear cookies at the end of the browsing session. There is private browsing mode where no cookies are stored permanently. Further technical restrictions for third-party cookies are expected to be implemented soon, and EU data protection rules also make storing cookies complicated to say the least.

So cookies are becoming increasingly unreliable. Fingerprinting is supposed to solve this issue by recognizing individual users without storing any data on their end. The idea is to look at data about user’s system that browsers make available anyway, for example display resolution. It doesn’t matter what the data is, it should be:

  • sufficiently stable, ideally stay unchanged for months
  • unique to a sufficiently small group of people

Note that no data point needs to identify a single person by itself. If each of them refer to a different group of people, with enough data points the intersection of all these groups will always be a single person.

How anti-fingerprinting is supposed to work

The goal of anti-fingerprinting is reducing the amount and quality of data that can be used for fingerprinting. For example, CSS used to allow recognizing websites that the user visited before – a design flaw that could be used for fingerprinting among other things. It took quite some time and effort, but eventually the browsers found a fix that wouldn’t break the web. Today this data point is no longer available to websites.

Other data points remain but have been defused considerably. For example, browsers provide websites with a user agent string so that these know e.g. which browser brand and version they are dealing with. Applications installed by the users used to extend this user agent string with their own identifiers. Eventually, browser vendors recognized how this could be misused for fingerprinting and decided to remove any third-party additions. Much of the other information originally available here has been removed as well, so that today any user agent string is usually common to a large group of people.

Barking the wrong tree

Browser vendors have already invested a considerable amount of work into anti-fingerprinting. However, they usually limited themselves to measures which wouldn’t break existing websites. And while things like display resolution (unlike window size) aren’t considered by too many websites, these were apparently numerous enough that browsers still give them user’s display resolution and the available space (typically display resolution without the taskbar).

Privacy protection extensions on the other hand aren’t showing as much concern. So they will typically do something like:

screen.width = 1280;
screen.height = 1024;

There you go, the website will now see the same display resolution for everybody, right? Well, that’s unless the website does this:

delete screen.width;
delete screen.height;

And suddenly screen.width and screen.height are restored to their original values. Fingerprinting can now use two data points instead of one: not merely the real display resolution but also the fake one. Even if that fake display resolution were extremely common, it would still make the fingerprint slightly more precise.

Is this magic? No, just how JavaScript prototypes work. See, these properties are not defined on the screen object itself, they are part of the object’s prototype. So that privacy extension added an override for prototype’s properties. With the override removed the original properties became visible again.

So is this the correct way to do it?

Object.defineProperty(Screen.prototype, "width", {value: 1280});
Object.defineProperty(Screen.prototype, "height", {value: 1024});

Much better. The website can no longer retrieve the original value easily. However, it can detect that the value has been manipulated by calling Object.getOwnPropertyDescriptor(Screen.prototype, "width"). Normally the resulting property descriptor would contain a getter, this one has a static value however. And the fact that a privacy extension is messing with the values is again a usable data point.

Let’s try it without changing the property descriptor:

Object.defineProperty(Screen.prototype, "width", {get: () => 1280});
Object.defineProperty(Screen.prototype, "height", {get: () => 1024});

Almost there. But now the website can call Object.getOwnPropertyDescriptor(Screen.prototype, "width").get.toString() to see the source code of our getter. Again a data point which could be used for fingerprinting. The source code needs to be hidden:

Object.defineProperty(Screen.prototype, "width", {get: (() => 1280).bind(null)});
Object.defineProperty(Screen.prototype, "height", {get: (() => 1024).bind(null)});

This bind() call makes sure the getter looks like a native function. Exactly what we needed.

Update (2020-12-14): Firefox allows content scripts to call exportFunction() which is a better way to do this. In particular, it doesn’t require injecting any code into web page context. Unfortunately, this functionality isn’t available in Chromium-based browsers. Thanks to kkapsner for pointing me towards this functionality.

Catching all those pesky frames

There is a complication here: a website doesn’t merely have one JavaScript execution context, it has one for each frame. So you have to make sure your content script runs in all these frames. And so browser extensions will typically specify "all_frames": true in their manifest. And that’s correct. But then the website does something like this:

var frame = document.createElement("iframe");
document.body.appendChild(frame);
console.log(screen.width, frame.contentWindow.screen.width);

Why is this newly created frame still reporting the original display width? We are back at square one: the website again has two data points to work with instead of one.

The problem here: if frame location isn’t set, the default is to load the special page about:blank. When Chrome developers created their extension APIs originally they didn’t give extensions any way to run content scripts here. Luckily, this loophole has been closed by now, but the extension manifest has to set "match_about_blank": true as well.

Timing woes

As anti-fingerprinting functionality in browser extensions is rather invasive, it is prone to breaking websites. So it is important to let users disable this functionality on specific websites. This is why you will often see code like this in extension content scripts:

chrome.runtime.sendMessage("AmIEnabled", function(enabled)
{
  if (enabled)
    init();
});

So rather than initializing all the anti-fingerprinting measures immediately, this content script first waits for the extension’s background page to tell it whether it is actually supposed to do anything. This gives the website the necessary time to store all the relevant values before these are changed. It could even come back later and check out the modified values as well – once again, two data points are better than one.

This is an important limitation of Chrome’s extension architecture which is sadly shared by all browsers today. It is possible to run a content script before any webpage scripts can run ("run_at": "document_start"). This will only be a static script however, not knowing any of the extension state. And requesting extension state takes time.

This might eventually get solved by dynamic content script support, a request originally created ten years ago. In the meantime however, it seems that the only viable solution is to initialize anti-fingerprinting immediately. If the extension later says “no, you are disabled” – well, then the content script will just have to undo all manipulations. But this approach makes sure that in the common scenario (functionality is enabled) websites won’t see two variants of the same data.

The art of faking

Let’s say that all the technical issues are solved. The mechanism for installing fake values works flawlessly. This still leaves a question: how does one choose the “right” fake value?

How about choosing a random value? My display resolution is 1661×3351, now fingerprint that! As funny as this is, fingerprinting doesn’t rely on data that makes sense. All it needs is data that is stable and sufficiently unique. And that display resolution is certainly extremely unique. Now one could come up with schemes to change this value regularly, but fact is: making users stand out isn’t the right way.

What you’d rather want is finding the largest group out there and joining it. My display resolution is 1920×1080 – just the common Full HD, nothing to see here! Want to know my available display space? I have my Windows taskbar at the bottom, just like everyone else. No, I didn’t resize it either. I’m just your average Joe.

The only trouble with this approach: the values have to be re-evaluated regularly. Two decades ago, 1024×768 was the most common display resolution and a good choice for anti-fingerprinting. Today, someone claiming to have this screen size would certainly stick out. Similarly, in my website logs visitors claiming to use Firefox 48 are noticeable: it might have been a common browser version some years ago, but today it’s usually bots merely pretending to be website visitors.

Comments

  • noname

    if my browser is returning a random fake value everytime my browser starts (like "return 1661x3351 this time and return 1920x1080 next time" but for canvas fingerprinting and others), would it makes me more identifiable (easier to trace) across every visit (I know that makes my browser stand out)?

    Wladimir Palant

    I don’t know about your habits, but my browser tends to stay open for a long time. So even with this rotation scheme, a random value here is problematic. Change the value more often and these changes might make you identifiable on their own (there is still your IP address which usually only changes daily). Using random values seems logical but it just isn’t the right way…

  • Andrew

    Wow! What a great article! Thanks a lot for the share! So can you recommend some good extension which can be used for anti-fingerprinting and which considers those points you've mentioned in this article? I'm using one at the moment (won't name it in order not to advertise) but not sure if it will "pass your test" :)

    Wladimir Palant

    No, I cannot, currently they literally all make these mistakes. Frankly, I’m not really convinced that they are worth it even when implemented correctly…

  • René Kåbis

    What got me into using Canvas Defender (both Chrome and Firefox) was its ability to rotate random values on a timer. So I set mine to about fifteen minutes.

    Now, I have absolutely no clue if it takes as much care to make retrieval of original values difficult, as you have shown here, but at least there is that auto-randomization of values on a set time period to fall back on, even though it doesn’t come configured as such out of the box.

    Wladimir Palant

    Well, rotating random values regularly has its issues – if you are one of the few people doing it, it will make you identifiable by itself. But defending against canvas-based fingerprinting is a complicated topic unfortunately, considerably more complicated than the approaches described here.

  • Ian Bradbury

    Thank you for this, I feel much more educated in this area now. I am sure it will have taken considerable time and effort.

  • zed

    Great article. By the way, "resistFingerprinting" in Firefox also works by spoofing screen size and browser features and is active by default in well known "hardened" user.js configs like arkenfox (formerly knows as ghacks). What do you think about it, does it also make fingerprinting easier? Thanks.

    Wladimir Palant

    No. While I haven’t looked into the implementation, it most certainly is proper. It rather has its own issues, often causing subtle and unintended website breakage. I’ve seen a bunch of bug reports that developers could trace back to resistFingerprinting, with much effort. The user typically has no chance to figure this out by themselves, and there also isn’t any mitigation other than disabling the setting globally. This might be fine for some users, but it certainly isn’t for everybody.

  • Jan

    Isn't it a fact that every browser extension (f.i. an adblocker like Adblock Plus) makes your fingerprint (more) unique?

    Wladimir Palant

    It isn’t quite that simple. Adblock Plus has been developed in such a way that it cannot be detected directly. This means in particular: no web_accessible_resources in the extension manifest. And even if web_accessible_resources were used, it only makes the extension only detectable in Chromium-based browsers, not Firefox.

    So websites can only detect Adblock Plus by its effects, meaning blocked requests. That’s more complicated and more error prone. And it also means that websites cannot determine which one of the many ad blockers you use. With ad blocker users ranging in hundreds of millions, this is hardly worth it.

    But – sure, many browser extensions make themselves detectable for no good reason. And that could add to your fingerprint.

  • Homenick90

    This is interesting theoretical perspective although in practice nobody would care to make an effort to additionally catch those 5 per million users who spoof something which makes anti-fingerprinting useful. It doesn't need to be perfect.

    Wladimir Palant

    That’s probably true. Though the fact that all of them make exactly the same mistakes might make it worthwhile – no extension-specific approach needed, a generic tampering detection will do.

  • Zm9!fSYkAn2

    Great article! I hope to read more on this topic from you.

    I noticed Screen.prototype.width should throw an error and defining width (with or without bind) removes the error.

    Wladimir Palant

    Yes, the solution here isn’t perfect. The point is mostly that a generic approach targeted at all fingerprinting extensions won’t produce useful results.

  • CanvasBlocker

    Disclaimer first: I am the developer of CanvasBlocker (won't link to addons.mozilla.org as it's easy enough to find)

    Your solution to the toString() problem also has a problem (at least in Firefox - not checked in Chrome as I do not care): the name of the function is wrong. It should be "get width". Also if you want to do some other protection you might need the actual object the method is called on. So the proper way to do this (in Firefox) is to use exportFunction. Then you can mimic the complete function attributes (toString(), name and length) if your function definition is correct.

    I think the sentence "Though the fact that all of them make exactly the same mistakes" in your reply above is incorrect. Despite the point in "The art of faking" (which I try to address as best as possible but cannot solve completely) CanvasBlocker shouldn't expose these mistakes (I have to confess it did in the past at one point).

    Wladimir Palant

    Is exportFunction() still available to extensions? It would be nice, but this doesn’t seem to be the case…

    Great to hear that CanvasBlocker does it better than the others, it will be a first for me. I actually started looking at your extension already because I noticed just how badly Canvas Defender messed up. I haven’t figured out the noise adding logic completely yet but I’ve seen that quite a bit of consideration went into it.

  • CanvasBlocker

    exportFunction is available for extensions on Firefox (not in Chrome - one of my points why CanvasBlocker is not available for Chrome).

    Yes - Canvas Defender is not a good extension and not active (last update in 2017).

    If you find anything in CanvasBlocker or have questions about it - feel free to open a discussion or issue at github.

    Wladimir Palant

    Yes, I already found the reincarnation of exportFunction() and added an update to the article. Thank you!

  • WatermelonFelon

    Exposure of extension spoofing via prototype lies - https://abrahamjuliot.github.io/creepjs/tests/prototype.html - https://github.com/abrahamjuliot/creepjs

    Wladimir Palant

    Seems to be rather Chrome-focused. On Firefox it detects 18 “lies” which are just browser differences from what I can tell.

  • WatermelonFelon

    Seems to be rather Chrome-focused [2] You can modify the depth of interrogation: see TZP [1] or the main creepJS test [2]. Even at full depth, any "false positives" are simply an extra fingerprint: e.g. it can distinguish Opera from Chrome. Works on Firefox, not just Chromium. Try something like Cydec or even just a simple user agent switcher and test on TZP (just click the lies to output to console)

    [1] https://arkenfox.github.io/TZP/tzp.html [2] https://abrahamjuliot.github.io/creepjs/index.html

    Cydec in Total Mode, on Firefox on TZP reveals 177 lies. Without Cydec, it is none

  • z0ccc

    I made the extension 'Vytal' that uses the debugger API to spoof data (instead of script tag injections). It fixes the issues described in the article.

    https://chrome.google.com/webstore/detail/vytal-spoof-timezone-loca/ncbknoohfjmcfneopnfkapmkblaenokb

    Wladimir Palant

    Nice to see that my research inspired some improvements.