<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Savannah Copland 👋</title>
    <description>The latest articles on Forem by Savannah Copland 👋 (@savannahjs).</description>
    <link>https://forem.com/savannahjs</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F552948%2Fd88a0ea1-2454-4d1a-9072-4def159751f7.png</url>
      <title>Forem: Savannah Copland 👋</title>
      <link>https://forem.com/savannahjs</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/savannahjs"/>
    <language>en</language>
    <item>
      <title>Why Browser Anti-Fingerprinting Techniques Are Not Effective</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Wed, 14 Sep 2022 19:55:10 +0000</pubDate>
      <link>https://forem.com/savannahjs/why-browser-anti-fingerprinting-techniques-are-not-effective-1n5f</link>
      <guid>https://forem.com/savannahjs/why-browser-anti-fingerprinting-techniques-are-not-effective-1n5f</guid>
      <description>&lt;p&gt;In this article, we will discuss why the existence of privacy-focused browsers doesn’t necessarily affect the effectiveness of fingerprint-based browser identification to prevent online fraud. &lt;/p&gt;

&lt;p&gt;We start with a technical dive into how today’s anti-fingerprinting solutions work, focusing on uniformity and privacy-through-randomization techniques and their specific implementations. We list multiple examples from popular browsers, including one of the more popular privacy-focused browsers, Brave. We then elaborate on why device identification remains a valuable tool to prevent online fraud. Let’s dive right in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anti-fingerprinting
&lt;/h2&gt;

&lt;p&gt;There are two common approaches when it comes to preventing browser fingerprinting. The more commonly implemented method makes all browser instances across all devices look as similar as possible. In theory, any fingerprinting should result in a single identifier or a very small number of different identifiers distributed across all devices, making the fingerprints useless. &lt;/p&gt;

&lt;p&gt;This is typically achieved by making functional changes to web APIs known to be good sources of entropy. As a result, some APIs are completely disabled because most websites don’t rely on them. Others are revised to return a dummy value regardless of the actual real value. As you can imagine, these practices dramatically change users’ web experience for the worse. &lt;/p&gt;

&lt;p&gt;However, some implementations hide the original functionality behind permission prompts. So the user can choose to let a website use a specific API in its original form, even though it might be used for fingerprinting (for example, the &lt;code&gt;privacy.resistFingerprinting.autoDeclineNoUserInputCanvasPrompts&lt;/code&gt;advanced preference option in Firefox, which prevents websites from reading canvas data). &lt;/p&gt;

&lt;p&gt;However, permission prompts are still not user-friendly, and most users do not understand the associated risks. Making a reasonable decision is, therefore, very hard. A good case in point is Chrome’s decision to &lt;a href="https://web.dev/persistent-storage/#chrome-and-other-chromium-based-browsers" rel="noopener noreferrer"&gt;handle some permission requests automatically&lt;/a&gt; without even notifying the user.&lt;/p&gt;

&lt;p&gt;The second approach aims to make every browser instance as distinct as possible through randomization. As a result, every browser gets a random fingerprint that changes between websites and browsing sessions or between page refreshes. This additional randomness makes it impossible to use fingerprints for reliable identification. The Brave browser, a mainstream privacy-focused Chromium fork, was the first popular browser to implement &lt;a href="https://brave.com/privacy-updates/4-fingerprinting-defenses-2.0/" rel="noopener noreferrer"&gt;privacy-through-randomization&lt;/a&gt; protections. One strongly voiced advantage of this approach is that sufficient randomization to prevent reliable fingerprinting can be done in a way that does not affect the web user experience. To better understand both approaches, let’s explore some examples.&lt;/p&gt;

&lt;h3&gt;
  
  
  Uniformity
&lt;/h3&gt;

&lt;p&gt;Gecko’s (the browser engine behind Firefox and the Tor Browser) fingerprinting protection is a prominent example of the uniformity approach in the real world. The feature is controlled by the &lt;code&gt;privacy.resistFingerprinting&lt;/code&gt; flag in the browser’s advanced preferences (&lt;code&gt;about:config&lt;/code&gt;). At the time of writing, this feature is still considered experimental and disabled by default in Firefox because, as mentioned in the &lt;a href="https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting" rel="noopener noreferrer"&gt;official documentation&lt;/a&gt;: “It is likely that it may degrade your Web experience.” Unlike the Tor Browser, which enables this feature for all security levels (the default being the “Standard” level).&lt;/p&gt;

&lt;p&gt;Under the hood, this flag controls what values are returned from various web APIs. In some cases, these values are fixed across all platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" rel="noopener noreferrer"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt; CSS media feature will always match “light.” &lt;/li&gt;
&lt;li&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/hardwareConcurrency" rel="noopener noreferrer"&gt;&lt;code&gt;navigator.hardwareConcurrency&lt;/code&gt;&lt;/a&gt; read-only property is set to 2.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/maxTouchPoints" rel="noopener noreferrer"&gt;&lt;code&gt;navigator.maxTouchPoints&lt;/code&gt;&lt;/a&gt; read-only property is set to 0.&lt;/li&gt;
&lt;li&gt;The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Screen/colorDepth" rel="noopener noreferrer"&gt;&lt;code&gt;screen.colorDepth&lt;/code&gt;&lt;/a&gt; read-only property is set to 24.&lt;/li&gt;
&lt;li&gt;The default time zone obtained by calling &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat" rel="noopener noreferrer"&gt;&lt;code&gt;window.Intl.DateTimeFormat()&lt;/code&gt;&lt;/a&gt; is always UTC.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, sometimes the spoofed, fixed values are platform-dependent. A comment in &lt;a href="https://github.com/mozilla/gecko-dev/blob/master/toolkit/components/resistfingerprinting/nsRFPService.h" rel="noopener noreferrer"&gt;Gecko’s source code&lt;/a&gt; explains why this is the case:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fseku9mdw4zqaca19s8lk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fseku9mdw4zqaca19s8lk.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As a result, calling &lt;code&gt;navigator.platform&lt;/code&gt; will return either &lt;code&gt;Win32&lt;/code&gt;, &lt;code&gt;MacIntel&lt;/code&gt;, &lt;code&gt;Linux aarch64&lt;/code&gt;, or &lt;code&gt;Linux x86_64&lt;/code&gt;, still concealing the actual platform but placing users into different buckets nonetheless. Similarly &lt;code&gt;navigator.userAgent&lt;/code&gt;, &lt;code&gt;navigator.appName&lt;/code&gt;, &lt;code&gt;navigator.appVersion&lt;/code&gt; and the Gecko-specific &lt;code&gt;navigator.oscpu&lt;/code&gt; will return spoofed values.&lt;/p&gt;

&lt;p&gt;Another good example of the uniformity approach to minimize fingerprint-able browser surface is the recent changes made to the deprecated &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/plugins" rel="noopener noreferrer"&gt;&lt;code&gt;navigator.plugins&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/mimeTypes" rel="noopener noreferrer"&gt;&lt;code&gt;navigator.mimeTypes&lt;/code&gt;&lt;/a&gt; features. The &lt;a href="https://html.spec.whatwg.org/multipage/" rel="noopener noreferrer"&gt;WHATWG HTML Standard&lt;/a&gt; was updated to reflect Flash deprecation and specified that browsers should always return a fixed list of supported plugins and mime types (depending on a new read-only &lt;code&gt;navigator.pdfViewerEnabled&lt;/code&gt; property). Gecko and Chromium have already adopted the change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Randomization
&lt;/h3&gt;

&lt;p&gt;One common technique used for browser fingerprinting is based on the internals of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API" rel="noopener noreferrer"&gt;Canvas API&lt;/a&gt;. A developer needs to create a canvas, draw a specially crafted image on it, then extract the image data and hash it into a short identifier. Subtle differences in the video card, graphics driver versions, and system-level properties like installed fonts, usually result in the hash representing a simple, stable, high-entropy browser fingerprint.&lt;/p&gt;

&lt;p&gt;The code can look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;canvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;122&lt;/span&gt;
&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;110&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;globalCompositeOperation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;multiply&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f2f&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#2ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ff2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;beginPath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;closePath&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f9c&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;evenodd&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toDataURL&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please see &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/846724bc368a562f5fb5fb2e6221e624329e55b6/src/sources/canvas.ts" rel="noopener noreferrer"&gt;this file&lt;/a&gt; from our open-source FingerprintJS library, for a production-ready example.&lt;/p&gt;

&lt;p&gt;The last step, calling the &lt;code&gt;HTMLCanvasElement.toDataURL()&lt;/code&gt; method, is when Brave’s anti-fingerprinting protections add randomization. The raw canvas data is passed through a method called PerturbPixels(). &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpeh8hpws9bkphym1dz57.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpeh8hpws9bkphym1dz57.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/brave/brave-core/blob/920bba00b6efb79aa6a1844c2dcca41eee066442/patches/third_party-blink-renderer-core-html-canvas-html_canvas_element.cc.patch" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4t6kzq60kkxlzhnta65.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi4t6kzq60kkxlzhnta65.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/brave/brave-core/blob/4c20d3c54a0e474f2d7a71d6843816744d957baa/chromium_src/third_party/blink/renderer/core/html/canvas/html_canvas_element.cc" rel="noopener noreferrer"&gt;S﻿ource&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The method uses a per-&lt;a href="https://web.dev/same-site-same-origin/" rel="noopener noreferrer"&gt;eTLD+1&lt;/a&gt; session key to change randomly selected bytes in the image data deterministically. This means that on different websites and whenever you restart the Brave browser, the data changes in a new way, meaning that the fingerprint based on this data changes, too. It is also worth calling out that the image data changes in an invisible way to humans, but because of how hash functions work, typical fingerprinting approaches yield different fingerprints.&lt;/p&gt;

&lt;p&gt;Here you can see how the image produced by the code above changes between Google Chrome and Brave:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93whn1pnruug2gi9mrjk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F93whn1pnruug2gi9mrjk.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, looking at the first 255 bytes of the underlying data, we can see the images are vastly different:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw27q2iq1qbg16tbpzmgf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw27q2iq1qbg16tbpzmgf.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Needless to say, Brave does the same thing in &lt;a href="https://github.com/brave/brave-browser/issues/9186" rel="noopener noreferrer"&gt;three other methods&lt;/a&gt; that are used to serialize raw canvas data: &lt;code&gt;CanvasRendering2dContext.getImageData&lt;/code&gt;, &lt;code&gt;HTMLCanvasElement.toBlob&lt;/code&gt;, and &lt;code&gt;OffscreenCanvas.convertToBlob&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Brave uses a similar approach for other sources of entropy, namely for the Web Audio APIs, when serializing data using the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AnalyserNode" rel="noopener noreferrer"&gt;AnalyserNode&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer" rel="noopener noreferrer"&gt;AudioBuffer&lt;/a&gt; interfaces and various WebGL APIs.&lt;/p&gt;

&lt;p&gt;Randomization is considerably simpler for simple sources of entropy, such as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Navigator/hardwareConcurrency" rel="noopener noreferrer"&gt;&lt;code&gt;navigator.hardwareConcurrency&lt;/code&gt;&lt;/a&gt; or the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme" rel="noopener noreferrer"&gt;&lt;code&gt;prefers-color-scheme&lt;/code&gt;&lt;/a&gt; CSS media future. For hardware concurrency, depending on the fingerprinting protection mode, which can be either “default” or “max,” Brave chooses a number randomly between 2 and the true value or 8. The preferred color scheme is always reported as “light” if the protection mode is “max.”&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/brave/brave-browser/wiki/Fingerprinting-Protections" rel="noopener noreferrer"&gt;Brave’s wiki pages&lt;/a&gt; show that the User-Agent, Plugins, and values returned by the Enumerate Devices APIs are also randomized. The Client Hints, Battery Status, Web Bluetooth APIs, and “HSTS fingerprinting” and “WebRTC IP leakage” are meant to be blocked instead. &lt;/p&gt;

&lt;p&gt;Interestingly, however, this wiki page is currently not fully up-to-date as WebRTC has &lt;a href="https://support.brave.com/hc/en-us/articles/360017989132-How-do-I-change-my-Privacy-Settings-#webrtc" rel="noopener noreferrer"&gt;dedicated settings&lt;/a&gt;. Instead, the &lt;a href="https://github.com/brave/brave-browser/issues/17651" rel="noopener noreferrer"&gt;Client Hints JavaScript API&lt;/a&gt;’s &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues" rel="noopener noreferrer"&gt;&lt;code&gt;getHighEntropyValues&lt;/code&gt;&lt;/a&gt; method returns accurate information, and the &lt;a href="https://github.com/brave/brave-core/blob/1cb5818aa0b70666c6aeea5ea9c06cc4e712171a/chromium_src/third_party/blink/renderer/modules/battery/battery_manager.cc" rel="noopener noreferrer"&gt;Battery Status API&lt;/a&gt; returns fixed values in all cases.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prevention Challenges
&lt;/h2&gt;

&lt;p&gt;As demonstrated in the examples above, using uniformity significantly reduces web experience. This can also be confirmed by looking at the relevant bug trackers, where users ask for options to disable anti-fingerprinting features, such as &lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1364261" rel="noopener noreferrer"&gt;spoofing the timezone&lt;/a&gt;. As a result, browsers that implement this approach tend to have a limited user base and do not represent a mainstream alternative to browsers without custom anti-fingerprinting features. Using randomization has its challenges and, in many cases, still affects the web experience negatively, especially for entropy sources that return a simple value, like a number or a boolean flag. For this reason, most users still have to choose between using a “standard” protection or “maximum” protection.\&lt;br&gt;
More importantly, the algorithms for randomization are prone to several errors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Websites can detect artificial randomization by calling the affected APIs twice, finding anomalies in the randomized data, or simply assuming that values are randomized by identifying the browser. Websites can afterward either ignore these values or process them in a way that aids fingerprinting.&lt;/li&gt;
&lt;li&gt;The original entropy from the raw source data still exists, and implementations adding additional randomness to the underlying data &lt;a href="https://fingerprint.com/blog/audio-fingerprinting/#reverting-brave-standard-farbling" rel="noopener noreferrer"&gt;might be reversible in practice&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;When other potentially indirect methods exist of learning about the raw source data, the original entropy can still be used without using one of the obvious ways to serialize the original data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both previously mentioned approaches, making all browser instances as similar as possible or as distinct as possible, fail to be effective unless they cover a significant number of potential entropy sources, which is quite hard to accomplish in real-world settings. And that proves to be very hard to do in real life.&lt;/p&gt;

&lt;p&gt;Generating reliable fingerprints depends on the kind of traffic a website has and the effectiveness of anti-fingerprinting features. For example, if most users use a popular browser like Google Chrome, you need more entropy sources to differentiate and identify different Chrome instances reliably. On the other hand, browsers with a smaller user base are more easily fingerprint-able. In those cases, ignoring randomized entropy sources is also feasible.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.wpoven.com/blog/browser-market-share/" rel="noopener noreferrer"&gt;wpoven.com&lt;/a&gt; and &lt;a href="https://kinsta.com/browser-market-share/" rel="noopener noreferrer"&gt;kinsta.com&lt;/a&gt; report that Brave has a market share of 0.05% for 2022. &lt;a href="https://www.ctrl.blog/entry/brave-market-share.html" rel="noopener noreferrer"&gt;ctrl.blog&lt;/a&gt; mentions a 1.02% share amongst its readers in 2020. In addition, brave reports 50.2 million active monthly users in 2021. &lt;/p&gt;

&lt;p&gt;For Firefox, &lt;a href="https://gs.statcounter.com/browser-market-share" rel="noopener noreferrer"&gt;statscounter&lt;/a&gt; reports a market share of 3.15% as of August 2022. &lt;a href="https://en.wikipedia.org/wiki/Usage_share_of_web_browsers" rel="noopener noreferrer"&gt;Wikipedia&lt;/a&gt; mentions claims from different sources as of October 2021, varying from 2.18% to 4.4%. Firefox’s &lt;a href="https://data.firefox.com/dashboard/user-activity" rel="noopener noreferrer"&gt;figures&lt;/a&gt; show around 200 million monthly active users.&lt;/p&gt;

&lt;p&gt;Based on our internal information from August 2022, traffic originating from Brave on Desktop and Android accounts for 1.57% of all identification events. For Firefox, it’s 1.997%. Interestingly, Firefox traffic matches the values spoofed by the &lt;code&gt;privacy.resistFingerprinting&lt;/code&gt; preference accounts for only 0.48% of all Firefox traffic we see. The Tor Browser accounts for 0.017% across all events.&lt;/p&gt;

&lt;p&gt;Entropy sources are also not limited to just inherent browser characteristics. It’s important to acknowledge that system-level and network-level characteristics are just as effective. Additionally, fingerprinting can be supported via storing client-side identifiers too. &lt;/p&gt;

&lt;p&gt;All this context is important to keep in mind when reasoning about the effectiveness of browser identification in the context of preventing online fraud.&lt;/p&gt;

&lt;p&gt;You might have noticed that when exploring Brave’s implementation of the privacy-through-randomization technique earlier, we ignored the per-eTLD+1 aspect of it. It prevents third-party cross-site fingerprinting, which is especially important for web tracking and advertising companies. However, it does not prevent a site from fingerprinting its visitors in the same-site (or first-party) context (that’s what the per-session aspect of the randomization tries to do).&lt;/p&gt;

&lt;h2&gt;
  
  
  Preventing online fraud
&lt;/h2&gt;

&lt;p&gt;Spamming comment sections, fake product reviews or coupons, cashback fraud, and abusing password reset and registration forms are all examples of online fraud that every big or small online service has to deal with inevitably. Even though all of these are very common, effective and scalable solutions prove to be very difficult to implement.&lt;/p&gt;

&lt;p&gt;A simple solution often comes to mind is to put the affected functionality behind authentication. This is a good strategy as long as the functionality wasn’t explicitly designed for unauthenticated visitors. Moreover, with authentication in place, you have to deal with a new problem; automated account creation and verification. At this point, many services think about using a CAPTCHA, whether it is to stop the automated account creation or the original form of abuse.&lt;/p&gt;

&lt;p&gt;Inevitably, CAPTCHAs will lead to a worse user experience and reduce conversion rates. However, more often than not, the abuse returns through other means. For example, when native mobile APIs expose the same functionality, CAPTCHAs won’t help, as their native apps support is essentially non-existent.&lt;/p&gt;

&lt;p&gt;Online services need reliable web browsers and native mobile device identification in these situations. In our experience, when done right, browser fingerprinting works wonders to prevent online fraud. Furthermore, because it happens in the background, it does not introduce additional friction unless a user is deemed fraudulent. To learn more about specific examples, you can read through our &lt;a href="https://fingerprintjs.com/case-studies/" rel="noopener noreferrer"&gt;case studies&lt;/a&gt;, &lt;a href="https://fingerprint.com/use-cases/" rel="noopener noreferrer"&gt;use case guides&lt;/a&gt;, or &lt;a href="https://fingerprintjs.com/contact-sales/" rel="noopener noreferrer"&gt;contact us&lt;/a&gt; and get hands-on guidance on incorporating Fingerprint Pro’s high accuracy identification API into your fraud prevention stack.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>github</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Exploiting IndexedDB API information leaks in Safari 15</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Mon, 17 Jan 2022 17:14:53 +0000</pubDate>
      <link>https://forem.com/savannahjs/exploiting-indexeddb-api-information-leaks-in-safari-15-58o2</link>
      <guid>https://forem.com/savannahjs/exploiting-indexeddb-api-information-leaks-in-safari-15-58o2</guid>
      <description>&lt;p&gt;&lt;small&gt;DISCLAIMER: FingerprintJS does not use this vulnerability in our products and does not provide cross-site tracking services. We focus on stopping fraud and support modern privacy trends for removing cross-site tracking entirely. We believe that vulnerabilities like this one should be discussed in the open to help browsers fix them as quickly as possible. To help fix it, we have submitted a bug report to the WebKit maintainers, created a live demo, and have made a &lt;a href="https://github.com/fingerprintjs/blog-indexeddb-safari-leaks-demo" rel="noopener noreferrer"&gt;public source code repository&lt;/a&gt; available to all.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=Z7dPeGpCl8s"&gt;Watch our Youtube overview&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;In this article, we discuss a software bug introduced in Safari 15’s implementation of the IndexedDB API that lets any website track your internet activity and even reveal your identity.&lt;/p&gt;

&lt;p&gt;We have also published a demo site to see the vulnerability in action:&lt;/p&gt;

&lt;p&gt;&lt;b&gt;&lt;a href="https://safarileaks.com"&gt;Try the demo&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

&lt;p&gt;The leak was reported to the &lt;a href="https://bugs.webkit.org/" rel="noopener noreferrer"&gt;WebKit Bug Tracker&lt;/a&gt; on November 28, 2021 as bug &lt;a href="https://bugs.webkit.org/show_bug.cgi?id=233548" rel="noopener noreferrer"&gt;233548&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update (Monday January 17th 2022)&lt;/strong&gt;: Apple engineers began working on the bug as of Sunday, have &lt;a href="https://github.com/WebKit/WebKit/commit/f73005ed826014988f8ee447de23927749fb56e5"&gt;merged potential fixes&lt;/a&gt;, and have marked our report as resolved. However, the bug continues to persist for end users until these changes are released.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short introduction to the IndexedDB API 
&lt;/h2&gt;

&lt;p&gt;IndexedDB is a browser API for client-side storage designed to hold significant amounts of data. It’s supported in all major browsers and is very commonly used. As IndexedDB is a low-level API, many developers choose to use wrappers that abstract most of the technicalities and provide an easier-to-use, more developer-friendly API. &lt;/p&gt;

&lt;p&gt;Like most modern web browser technologies, IndexedDB is following &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy" rel="noopener noreferrer"&gt;Same-origin policy&lt;/a&gt;. The same-origin policy is a fundamental security mechanism that restricts how documents or scripts loaded from one origin can interact with resources from other origins. An origin is defined by the scheme (protocol), hostname (domain), and port of the URL used to access it. &lt;/p&gt;

&lt;p&gt;Indexed databases are associated with a specific origin. Documents or scripts associated with different origins should never have the possibility to interact with databases associated with other origins.&lt;/p&gt;

&lt;p&gt;If you want to learn more about how IndexedDB APIs work check out the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API" rel="noopener noreferrer"&gt;MDN Web Docs&lt;/a&gt; or the &lt;a href="https://www.w3.org/TR/IndexedDB/" rel="noopener noreferrer"&gt;W3C specification&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The IndexedDB leaks in Safari 15
&lt;/h2&gt;

&lt;p&gt;In Safari 15 on macOS, and in all browsers on iOS and iPadOS 15, the IndexedDB API is violating the same-origin policy. Every time a website interacts with a database, a new (empty) database with the same name is created in all other active frames, tabs, and windows within the same browser session. Windows and tabs usually share the same session, unless you switch to a different profile, in Chrome for example, or open a private window. For clarity, we will refer to the newly created databases as “cross-origin-duplicated databases” for the remainder of the article.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why is this leak bad?
&lt;/h3&gt;

&lt;p&gt;The fact that database names leak across different origins is an obvious privacy violation. It lets arbitrary websites learn what websites the user visits in different tabs or windows. This is possible because database names are typically unique and website-specific. Moreover, we observed that in some cases, websites use unique user-specific identifiers in database names. This means that authenticated users can be uniquely and precisely identified. Some popular examples would be YouTube, Google Calendar, or Google Keep. All of these websites create databases that include the authenticated Google User ID and in case the user is logged into multiple accounts, databases are created for all these accounts.&lt;/p&gt;

&lt;p&gt;The Google User ID is an internal identifier generated by Google. It uniquely identifies a single Google account. It can be used with Google APIs to fetch public personal information of the account owner. The information exposed by these APIs is controlled by many factors. In general, at minimum, the user's profile picture is typically available. To learn more, refer to Google's &lt;a href="https://developers.google.com/people/v1/how-tos/authorizing#profile-scopes" rel="noopener noreferrer"&gt;People API&lt;/a&gt; documentation.&lt;/p&gt;

&lt;p&gt;Not only does this imply that untrusted or malicious websites can learn a user’s identity, but it also allows the linking together of multiple separate accounts used by the same user.&lt;/p&gt;

&lt;p&gt;Note that these leaks do not require any specific user action. A tab or window that runs in the background and continually queries the IndexedDB API for available databases, can learn what other websites a user visits in real-time. Alternatively, websites can open any website in an iframe or popup window in order to trigger an IndexedDB-based leak for that specific site.&lt;/p&gt;

&lt;h3&gt;
  
  
  How many websites are affected?
&lt;/h3&gt;

&lt;p&gt;We checked the homepages of Alexa’s Top 1000 most visited websites to understand how many websites use IndexedDB and can be uniquely identified by the databases they interact with. &lt;/p&gt;

&lt;p&gt;The results show that more than 30 websites interact with indexed databases directly on their homepage, without any additional user interaction or the need to authenticate. We suspect this number to be significantly higher in real-world scenarios as websites can interact with databases on subpages, after specific user actions, or on authenticated parts of the page.&lt;/p&gt;

&lt;p&gt;We also saw a pattern where indexed databases named as universally unique identifiers (UUIDs) are being created by subresources, specifically ad networks. Interestingly, loading of these resources seems to be in some cases blocked by Safari’s tracking prevention features, which effectively prevents the database names from leaking. These leaks will also be prevented if the resources are blocked by other means, for example when using adblocker extensions or blocking all JavaScript execution.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Safari private mode protect against the leak?
&lt;/h3&gt;

&lt;p&gt;Firstly, when followed, the same-origin policy is an effective security mechanism for all window modes. Websites with one origin should never have access to resources from other origins regardless of whether a visitor is using private browsing or not unless it’s explicitly allowed via cross-origin resource sharing (CORS).&lt;/p&gt;

&lt;p&gt;In this case, private mode in Safari 15 is also affected by the leak. It’s important to note that browsing sessions in private Safari windows are restricted to a single tab, which reduces the extent of information available via the leak. However, if you visit multiple different websites within the same tab, all databases these websites interact with are leaked to all subsequently visited websites. Note that in other WebKit-based browsers, for example Brave or Google Chrome on iOS, private tabs share the same browser session in the same way as in non-private mode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;We created a simple demo page that demonstrates how a website can learn the Google account identity of any visitor. The demo is available at &lt;a href="https://safarileaks.com"&gt;safarileaks.com&lt;/a&gt;. If you open the page and start the demo in an affected browser, you will see how the current browsing context and your identity is leaked right away. Identity data will only be available if you are authenticated to your Google account in the same browsing session. &lt;/p&gt;

&lt;p&gt;Moreover, the demo detects the presence of 20+ websites in other browser tabs or windows, including Google Calendar, Youtube, Twitter, and Bloomberg. This is possible because database names, which those websites interact with, are specific enough to uniquely identify them.  &lt;/p&gt;

&lt;p&gt;The supported browsers are Safari 15 on macOS, and essentially all browsers on iOS 15 and iPadOS 15. That is because Apple’s App Store guidelines require all browsers on iOS  and iPadOS to use the WebKit engine.&lt;/p&gt;

&lt;p&gt;&lt;b&gt;&lt;a href="https://safarileaks.com"&gt;Try the demo&lt;/a&gt;&lt;/b&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproducing the leak
&lt;/h2&gt;

&lt;p&gt;To reproduce the leak yourself, simply call the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/databases" rel="noopener noreferrer"&gt;indexedDB.databases()&lt;/a&gt; API. In case websites opened in other frames, tabs, or windows interact with other databases, you will see the cross-origin-duplicated databases.&lt;/p&gt;

&lt;p&gt;Based on our observations, if a database is deleted, all related cross-origin-duplicated databases are also deleted. However, there seems to be an issue when developer tools are opened and a page refresh happens. On every page refresh, all databases are duplicated once again and seem to become independent from the original databases. In fact, it’s not even possible to delete these duplicated databases by using the regular &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/IDBFactory/deleteDatabase" rel="noopener noreferrer"&gt;indexedDB.deleteDatabase()&lt;/a&gt; function. &lt;/p&gt;

&lt;p&gt;This behavior makes it very difficult to use the developer tools to understand what exactly is happening with the databases that a website interacts with. It is therefore recommended to use other means of debugging (for example rendering output into the DOM instead of using console logs or the JavaScript debugger) when trying to reproduce the leaks described in this article. &lt;/p&gt;

&lt;h2&gt;
  
  
  How to protect yourself
&lt;/h2&gt;

&lt;p&gt;Unfortunately, there isn’t much Safari, iPadOS and iOS users can do to protect themselves without taking drastic measures. One option may be to block all JavaScript by default and only allow it on sites that are trusted. This makes modern web browsing inconvenient and is likely not a good solution for everyone. Moreover, vulnerabilities like cross-site scripting make it possible to get targeted via trusted sites as well, although the risk is much smaller. Another alternative for Safari users on Macs is to temporarily switch to a different browser. Unfortunately, on iOS and iPadOS this is not an option as all browsers are affected.&lt;/p&gt;

&lt;p&gt;The only real protection is to update your browser or OS once the issue is resolved by Apple. In the meantime, we hope this article will raise awareness of this issue.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Demo: Disabling JavaScript Won’t Save You from Fingerprinting</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Thu, 21 Oct 2021 18:08:06 +0000</pubDate>
      <link>https://forem.com/savannahjs/demo-disabling-javascript-wont-save-you-from-fingerprinting-4838</link>
      <guid>https://forem.com/savannahjs/demo-disabling-javascript-wont-save-you-from-fingerprinting-4838</guid>
      <description>&lt;p&gt;Fingerprinting is a way to identify website users without using cookies or data storage. Instead, device properties like language and installed fonts are used to create highly accurate, unique identifiers that work even if the browser has incognito mode turned on.&lt;/p&gt;

&lt;p&gt;A common misconception is that disabling JavaScript can prevent fingerprinting. Since advertisers and bad actors use it for ad targeting and tracking your online activity, it’s a natural (albeit incorrect) assumption that disabling JavaScript will protect you against fingerprinting. In this article, we will demonstrate that fingerprinting can occur even in the absence of JavaScript.&lt;/p&gt;

&lt;p&gt;Check out the demo to see it in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://noscriptfingerprint.com" rel="noopener noreferrer"&gt;https://noscriptfingerprint.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The demo should show the same fingerprint, even if visitors attempt to conceal their identities using the following methods (among others):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requesting desktop mode in mobile browsers&lt;/li&gt;
&lt;li&gt;Spoofing the user agent&lt;/li&gt;
&lt;li&gt;Using incognito mode&lt;/li&gt;
&lt;li&gt;Changing the internet connection&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are just a handful of the many use cases where fingerprinting can uniquely identify devices, even as other methods fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the demo works
&lt;/h2&gt;

&lt;p&gt;When you open the main page of the demo, your browser sends several HTTP requests to the demo’s server automatically. The list of requests and the request contents depend on your specific device and browser (more on this later). The server extracts meaningful pieces of data — or signals — from the HTTP requests and stores them in a database. Your device signals stay the same as you visit different websites and subsequently can be used to reliably identify and track you over time.&lt;/p&gt;

&lt;p&gt;The server links the requests of a single visitor together using a unique random token by inserting this token into the main page’s HTML code. As a result, all HTTP requests from the main page contain the token, and different visitors requesting the main page receive unique pieces of HTML code.&lt;/p&gt;

&lt;p&gt;Here’s a simplified example of how this works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getMainPageHTML&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;makeRandomString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`&amp;lt;html&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;img src="/image/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;" /&amp;gt;
    &amp;lt;iframe src="/frame/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&amp;lt;/iframe&amp;gt;
    &amp;lt;a href=”/result/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;token&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;”&amp;gt;See the fingerprint&amp;lt;/a&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;`&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your fingerprint is shown on a separate page. The URL contains the token as well. The server finds your signals using this token, calculates a hash sum using all of the signals, and returns the result to the browser (the hash sum is the fingerprint).&lt;/p&gt;

&lt;p&gt;In our demo, the page is placed inside an iframe to make the fingerprint viewable on the main page, but keep in mind that the server can access the fingerprint behind the scenes, at any moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  No-JavaScript signal sources
&lt;/h2&gt;

&lt;p&gt;The following is a list of signal sources that don’t require JavaScript; however, not all signals listed are included in the demo, largely due to their low contribution to accuracy or inherent instability.&lt;/p&gt;

&lt;h3&gt;
  
  
  IP address (not included in demo)
&lt;/h3&gt;

&lt;p&gt;The server receives your IP address with every HTTP request. Typically, IP addresses are unique but are considered unstable: when your underlying internet connection changes (e.g., from Wi-Fi to cellular) or VPN is turned on, your device IP address changes as well. For this reason, IP address is not used as a signal in the demo.&lt;/p&gt;

&lt;h3&gt;
  
  
  HTTP headers
&lt;/h3&gt;

&lt;p&gt;HTTP headers are a part of every HTTP request and response — they come before the body (i.e., the payload) and consist of name/value pairs separated by colons. This meta information enables better communication between the browser and the server. Some HTTP request headers contain information about the user’s browser settings. The demo uses these header values as signals.&lt;/p&gt;

&lt;p&gt;The following illustration depicts a browser HTTP request and a server HTTP response when a user visits example.com:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys8im83srekhi9jxyw2q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fys8im83srekhi9jxyw2q.png" alt="HTTP request and response"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can view the headers under the Network section of your browser’s development toolbox.&lt;br&gt;
Browsers send these headers with every HTTP request; in turn, the backend can parse signals from these headers from any HTTP request, including the request for the main page.&lt;/p&gt;
&lt;h3&gt;
  
  
  User-Agent (not included in demo)
&lt;/h3&gt;

&lt;p&gt;This HTTP header signal contains detailed information about the browser version, operating system, and other device-related information. This header value is considered unstable, as mobile browsers alter it when a desktop version of the website is requested. Additionally, Safari provides an easy way to change the user-agent value and many privacy-related browser extensions spoof the user-agent. For these reasons, user-agent isn’t used in the demo.&lt;/p&gt;
&lt;h3&gt;
  
  
  Accept
&lt;/h3&gt;

&lt;p&gt;Browsers use this HTTP header value to tell servers what file types are supported. &lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accept: text/html,application/xhtml+xml,application/xml;q=0.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file types supported depends on the browser engine and version. Browsers send different header values for different types of resources (e.g., webpages, images, stylesheets, video and audio). The demo makes individual requests to get Accept header values for each resource type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;link&lt;/span&gt; &lt;span class="na"&gt;rel=&lt;/span&gt;&lt;span class="s"&gt;"stylesheet"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/headers/(token)/style"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"/headers/(token)/style"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No separate request for the web page header is needed because browsers send the header with the request for the main page.&lt;/p&gt;

&lt;p&gt;The demo doesn’t use &lt;code&gt;&amp;lt;audio&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;video&amp;gt;&lt;/code&gt; tags,  since media requests aren’t made when the page is invisible; if these tags were used, the page would produce a different fingerprint when visible. Also, the Accept header value for audio/video requests never changes in a single browser engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accept-Language
&lt;/h3&gt;

&lt;p&gt;This HTTP header value tells the server what languages the client prefers. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accept-Language: en-GB,en-US;q=0.9,en;q=0.8,ru;q=0.7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google Chrome only sends one language in incognito mode, so the demo uses the first language as a signal.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accept-Encoding
&lt;/h3&gt;

&lt;p&gt;This HTTP header value advertises which content encoding (e.g., compression algorithm) the browser is able to understand and varies with browser engine/version. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accept-Encoding: gzip, deflate, br
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Client Hints (not included in demo)
&lt;/h3&gt;

&lt;p&gt;Client Hints are special HTTP headers. Browsers don’t send these by default — if the server responds with an &lt;code&gt;Accept-CH&lt;/code&gt; header, the browser will add the Client Hints to future requests to this website. For example, this response header makes browsers send a &lt;code&gt;Device-Memory&lt;/code&gt; and a &lt;code&gt;Sec-CH-UA-Full-Version&lt;/code&gt; header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Accept-CH: Device-Memory, Sec-CH-UA-Full-Version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Currently, only Google Chrome and other Chromium-based browsers support this header. Chromium browsers don’t send Client Hints when JavaScript is disabled, thus Client Hints aren’t used in the demo.&lt;/p&gt;

&lt;h3&gt;
  
  
  CSS
&lt;/h3&gt;

&lt;p&gt;The demo collects several signals using the browser’s CSS engine. All the CSS signals work the same way: the page’s CSS code determines whether or not to send an HTTP request based on the browser, OS, device, and other parameters. In general, the CSS code for a signal looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"css_probe_42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;magic-query&lt;/span&gt; &lt;span class="nc"&gt;.css_probe_42&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/signal/(token)&lt;/span&gt;&lt;span class="p"&gt;/(&lt;/span&gt;&lt;span class="n"&gt;signalName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your browser matches the &lt;code&gt;magic-query&lt;/code&gt; CSS selector, it will apply the &lt;code&gt;background&lt;/code&gt; style to the &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; tag and make an HTTP request to download the background image (with the URL &lt;code&gt;/signal/(token)/(signalName)&lt;/code&gt;). The server will then save this information in the database. If your browser doesn’t match the selector, the server will treat the absence of a request as a signal as well. &lt;code&gt;css_probe_42&lt;/code&gt; is a random class name for a signal, as every signal must have a unique class name.&lt;/p&gt;

&lt;p&gt;The magic selectors used in the demo are described in the following section.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feature queries
&lt;/h3&gt;

&lt;p&gt;A special CSS rule called &lt;code&gt;@supports&lt;/code&gt; applies CSS styles only if the browser supports the given feature. Different browsers vary in terms of their features, so these can be used to tell browser engines apart.&lt;br&gt;
This is CSS code that will only trigger an HTML request in Chromium-based browsers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@supports&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;-webkit-app-region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;inherit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.css_probe_42&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url(...)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Other features (in place of &lt;code&gt;-webkit-app-region&lt;/code&gt;) used in the demo include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-moz-appearance&lt;/code&gt; detects Firefox and other browsers with the Gecko engine&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-apple-pay-button-style&lt;/code&gt; detects Safari&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-webkit-touch-callout&lt;/code&gt; detects any iOS browser&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-moz-osx-font-smoothing&lt;/code&gt; detects macOS Firefox&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;accent-color&lt;/code&gt; detects modern Chromium (version 93+) and Gecko (version 92+) browsers. Since Tor uses an outdated version of Gecko, the absence of this feature indicates that the browser is most likely Tor.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Media queries
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;@media&lt;/code&gt; is a CSS keyword that enables the application of CSS styles based on various conditions outside the page. In general, CSS code with a media query looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.selector&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the &lt;code&gt;feature&lt;/code&gt; matches the &lt;code&gt;value&lt;/code&gt;, the interior CSS code is applied to the page. A feature may have multiple possible values, so the demo can produce various HTTP requests depending on the feature value. The browser either makes one of the requests, or none at all.&lt;/p&gt;

&lt;p&gt;This is what the CSS code looks like in general:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"css_probe_42"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;featureX&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;value1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.css_probe_42&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/signal/(token)&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;featureX&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;value1&lt;/span&gt;&lt;span class="s2"&gt;');
    }
  }
  @media (featureX: value2) {
    .css_probe_42 {
      background: url('&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;/(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)/&lt;/span&gt;&lt;span class="n"&gt;featureX&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;value2&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c"&gt;/* ... */&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo uses the following media features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;hover&lt;/code&gt; and &lt;code&gt;any-hover&lt;/code&gt; indicate whether the device allows users to hover over HTML elements&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pointer&lt;/code&gt; and &lt;code&gt;any-pointer&lt;/code&gt; indicate whether the device has a pointing device (e.g., a mouse) and how accurate it is.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;color&lt;/code&gt; indicates whether the device’s screen supports colors and how many bits are used in a single color channel of the screen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;color-gamut&lt;/code&gt; denotes the color space the device’s screen is capable of&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;forced-colors&lt;/code&gt; indicates whether the browser is set up to restrict the color palette&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;inverted-colors&lt;/code&gt; indicates whether the operating system inverts the screen colors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;monochrome&lt;/code&gt; indicates whether the screen is monochrome — either naturally or because of operating system settings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefers-color-scheme&lt;/code&gt; indicates whether the user has chosen the light or the dark theme in the operating system settings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefers-contrast&lt;/code&gt; indicates whether the user has asked the system to increase or decrease the amount of contrast between adjacent colors&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt; indicates the user’s preference in having less motion on the screen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;dynamic-range&lt;/code&gt; indicates whether the display supports HDR&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next set of features are a bit trickier: &lt;code&gt;device-width&lt;/code&gt;, &lt;code&gt;device-height&lt;/code&gt; and &lt;code&gt;-webkit-device-pixel-ratio&lt;/code&gt; reflect the resolution of the whole screen and its pixel density. The values of these features are arbitrary — you can write CSS code that has a &lt;code&gt;@media&lt;/code&gt; rule for all the thousands of possible values, but it will only add unnecessary bloat to your code base. Instead, the demo checks ranges of values using the &lt;code&gt;min&lt;/code&gt; and &lt;code&gt;max&lt;/code&gt; rules. &lt;/p&gt;

&lt;p&gt;Below is an example of how to detect screen width:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;349.99px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.css_probe_42&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/signal/(token)&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;screenWidth&lt;/span&gt;&lt;span class="p"&gt;/,&lt;/span&gt;&lt;span class="m"&gt;350&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;350px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;999.99px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.css_probe_42&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/signal/(token)&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;screenWidth&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;350&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2499.99px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.css_probe_42&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/signal/(token)&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;screenWidth&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="m"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;@media&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2500px&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nc"&gt;.css_probe_42&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sx"&gt;url('/signal/(token)&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;screenWidth&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="m"&gt;2500&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;More range entries with narrower values will result in a higher degree of precision.&lt;/p&gt;

&lt;p&gt;The screen width and height values of an Android device will swap when it’s  rotated from portrait orientation to landscape, and vice-versa. In order to preserve the fingerprint, the demo swaps the values in order to make the width always be smaller than the height.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fonts
&lt;/h3&gt;

&lt;p&gt;Operating systems have a myriad of different built-in fonts; additionally, desktops systems typically allow users to add their own custom fonts. It’s impossible to retrieve a list of all the user’s fonts without JavaScript (and the user’s permission), but it is possible to check whether a specific font is installed.&lt;/p&gt;

&lt;p&gt;A CSS rule called font-face adds a custom font for use on the web page. The rule includes a set of font names to search the device for and a URL of the font file. If an installed font with the given name is found, the browser will use it; otherwise it will download the font file from the specified URL. For this reason, the server can make the determination that the font is missing if the URL has been requested. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font-face: 'Helvetica';"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;a&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;@font-face&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Helvetica'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;src&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;local&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;'Helvetica'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
         &lt;span class="sx"&gt;url('/signal/(token)&lt;/span&gt;&lt;span class="p"&gt;/&lt;/span&gt;&lt;span class="n"&gt;fontHelvetica&lt;/span&gt;&lt;span class="s2"&gt;') format('&lt;/span&gt;&lt;span class="n"&gt;truetype&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The demo uses the following fonts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Roboto&lt;/code&gt; and &lt;code&gt;Ubuntu&lt;/code&gt; are available on Android and ChromeOS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ubuntu&lt;/code&gt; is available on Ubuntu&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Calibri&lt;/code&gt; and &lt;code&gt;MS UI Gothic&lt;/code&gt; are available on Windows&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Gill Sans&lt;/code&gt; and &lt;code&gt;Helvetica Neue&lt;/code&gt; are available on macOS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Arimo&lt;/code&gt; is available on ChromeOS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As you can see, installed fonts are an effective way to tell operating systems apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Disabling JavaScript doesn’t prevent your device from being fingerprinted, as most browsers will still leak an abundance of data such as IP addresses, behavior patterns, and more. And since most websites require JavaScript to function properly, using this method to preserve your online privacy will invariably lead to a suboptimal web experience. &lt;/p&gt;

&lt;p&gt;Special browsers like Tor guarantee anonymity and the exact same fingerprint across all users; for those that take privacy to the extremes, this may be the only option.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Star, follow, or fork our &lt;a href="https://github.com/fingerprintjs/blog-nojs-fingerprint-demo" rel="noopener noreferrer"&gt;no JavaScript fingerprinting demo&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email any questions you have to &lt;a href="mailto:oss@fingerprintJS.com"&gt;&lt;/a&gt;&lt;a href="mailto:oss@fingerprintJS.com"&gt;oss@fingerprintJS.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Join our &lt;a href="https://discord.gg/ad6R2ttHVX" rel="noopener noreferrer"&gt;Discord channel&lt;/a&gt; to discuss all things FingerprintJS, cybersecurity, and privacy&lt;/li&gt;
&lt;li&gt;Join our team and work on exciting research in device security: &lt;a href="mailto:work@fingerprintjs.com"&gt;&lt;/a&gt;&lt;a href="mailto:work@fingerprintjs.com"&gt;work@fingerprintjs.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>tutorial</category>
      <category>webdev</category>
      <category>css</category>
    </item>
    <item>
      <title>How Android Wallpaper Images Can Threaten Your Privacy</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Tue, 05 Oct 2021 21:33:47 +0000</pubDate>
      <link>https://forem.com/savannahjs/how-android-wallpaper-images-can-threaten-your-privacy-n83</link>
      <guid>https://forem.com/savannahjs/how-android-wallpaper-images-can-threaten-your-privacy-n83</guid>
      <description>&lt;p&gt;Android 12’s highly anticipated Material You design system features wallpaper-based color theming and advanced customizations powered by color extraction. These UI enhancements allow users to select a wallpaper (i.e., a personal background image) from which an optimal palette of colors is automatically generated and applied to the device’s look and feel globally.&lt;/p&gt;

&lt;p&gt;Unfortunately, such personalization can carry a high price in compromised privacy. In this article, we’ll demonstrate how Android wallpapers can be used to track users and explore ways to prevent your device from being exploited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Android wallpaper images vs. user privacy
&lt;/h2&gt;

&lt;p&gt;The WallpaperManager class was introduced in 2009 as part of the release of Android 2, API version 5. The class provides methods for interacting with wallpapers, including &lt;a href="https://developer.android.com/reference/android/app/WallpaperManager#getDrawable()" rel="noopener noreferrer"&gt;getDrawable()&lt;/a&gt; for retrieving the current system wallpaper as a drawable resource.&lt;/p&gt;

&lt;p&gt;Using the following code, a drawable resource can be represented as a byte array:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;private fun calculateWallpaperBytes(): ByteArray {
   val imageBitmap = wallpaperManager.drawable.toBitmap()
   val stream = ByteArrayOutputStream()
   imageBitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
   return stream.toByteArray()
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Byte arrays can be used to restore original images from Android wallpapers, which are highly likely to contain personal information or details uniquely important to the user. Every app on your device can view and download photos of your family, pets, favorite bands or movies, and anything else you may have set as a wallpaper. Moreover, you couldn’t prevent them from doing so before Android 8.1.&lt;/p&gt;

&lt;p&gt;As it stands, a large percentage of devices are running Android 8.1 or earlier (almost 44.6% at the time of this writing, per Google Analytics) and still vulnerable to this exploit.&lt;/p&gt;

&lt;h3&gt;
  
  
  A new color extraction method
&lt;/h3&gt;

&lt;p&gt;Starting with Android 8.1, the &lt;a href="https://developer.android.com/reference/android/app/WallpaperManager#getDrawable()" rel="noopener noreferrer"&gt;getDrawable()&lt;/a&gt; method requires the use of &lt;a href="https://developer.android.com/reference/android/Manifest.permission#READ_EXTERNAL_STORAGE" rel="noopener noreferrer"&gt;READ_EXTERNAL_STORAGE&lt;/a&gt;, a less insecure but nonetheless risky permission as it enables access to all media on a device (and more privileged data). To compensate for the limited functionality, an easier way to extract colors was also introduced in Android 8.1: &lt;a href="https://developer.android.com/reference/android/app/WallpaperManager#getWallpaperColors(int)" rel="noopener noreferrer"&gt;getWallpaperColors(int which)&lt;/a&gt;,  which returns 3 main colors from a wallpaper image.&lt;/p&gt;

&lt;p&gt;Like iOS, Android allows users to determine which specific screens to use wallpaper images, and the integer argument “which” sets which exact wallpaper image to use for color extraction. There are two options: the constant values &lt;code&gt;WallpaperManager.FLAG_SYSTEM&lt;/code&gt; or &lt;code&gt;WallpaperManager.FLAG_LOCK&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotwuy03uwxyjr6h431cu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fotwuy03uwxyjr6h431cu.png" alt="Wallpaper settings"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// WallpaperManager.FLAG_LOCK for the the lock screen

val colors = WallpaperManager

.getInstance(context).getWallpaperColors(WallpaperManager.FLAG_SYSTEM) 

val primaryColor: Int = colors.primaryColor.toArgb()

val secondaryColor: Int = colors.secondaryColor.toArgb()

val tertiaryColor: Int = colors.tertiaryColor.toArgb()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above code illustrates how to extract colors using a &lt;a href="https://developer.android.com/reference/android/content/Context" rel="noopener noreferrer"&gt;context&lt;/a&gt; object with primary, secondary and tertiary colors corresponding to the most popular colors in the picture (primary being the most popular). Note that no special permissions are required to use this new method.&lt;/p&gt;

&lt;p&gt;The following is an example of color extraction using the new method with a real picture:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk1d7ukoeva75gmwlezsh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fk1d7ukoeva75gmwlezsh.png" alt="Koala color extraction"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The methods may return null in some scenarios (e.g., when custom launchers redefine wallpaper management logic without using the &lt;code&gt;WallpaperManager&lt;/code&gt; class). However, if a wallpaper was set once by &lt;code&gt;WallpaperManager&lt;/code&gt;, the method will return a not-null value.&lt;/p&gt;

&lt;h3&gt;
  
  
  The science of color extraction
&lt;/h3&gt;

&lt;p&gt;Since Android is open source, we can readily determine how the method actually works. According to the &lt;a href="https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/com/android/internal/graphics/palette/VariationalKMeansQuantizer.java;l=31?q=KMeansQua&amp;amp;sq=&amp;amp;ss=android%2Fplatform%2Fsuperproject" rel="noopener noreferrer"&gt;code&lt;/a&gt;, colors are the result of work of Variational &lt;a href="https://en.wikipedia.org/wiki/K-means_clustering" rel="noopener noreferrer"&gt;K-means&lt;/a&gt; quantizer. Every image pixel is represented by a color and every color is a &lt;a href="https://en.wikipedia.org/wiki/Three-dimensional_space" rel="noopener noreferrer"&gt;3-dimensional&lt;/a&gt; point in space (e.g., RGB color space). All pixels form a set in space, and the algorithm performs clustering of the set on K parts with finding K points, which are equidistant from others in the set.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhywo70avr8i7ojxe8ie9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhywo70avr8i7ojxe8ie9.png" alt="How K-means method works"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Above is a visualization of how the K-means method works, courtesy of &lt;a href="https://vas3k.com/blog/machine_learning/" rel="noopener noreferrer"&gt;vas3k&lt;/a&gt;. This particular case is 3-means in a 2-dimensional space.&lt;/p&gt;

&lt;p&gt;In the case of Android, colors are represented in the &lt;a href="https://en.wikipedia.org/wiki/HSL_and_HSV" rel="noopener noreferrer"&gt;HSL&lt;/a&gt; color space and distance is calculated using classical measures of &lt;a href="https://en.wikipedia.org/wiki/Euclidean_distance" rel="noopener noreferrer"&gt;euclidean distance&lt;/a&gt;. The results are three shades of an image  that are  equidistant (in the color space) from every pixel of the image. &lt;/p&gt;

&lt;h3&gt;
  
  
  A universe of combinations
&lt;/h3&gt;

&lt;p&gt;This color extraction algorithm is basically a map from the set of all possible images to the RGB color space. The set is infinite and the RGB color space is limited by 2²⁴ combinations. Theoretically, this means every RGB combination is possible. Every color is represented by 32 bits, but only 24 matter. Alpha channels will always be equal to 1 (according to sources), while every component of the colors R, G and B have 256 possible combinations, or 2⁸. &lt;/p&gt;

&lt;p&gt;Since each component is independent, we can directly multiply the number of combinations together. This comes out to: 2⁸ * 2⁸ * 2⁸ = 2²⁴ combinations for every color. We have 3 colors for a image, and 2²⁴ * 2²⁴ * 2²⁴ = 2⁷² combinations per image.&lt;/p&gt;

&lt;p&gt;The same logic applies to the second wallpaper image, and they can be set up independently of each other. From one wallpaper image we have 72 bits and 144 bits using system wallpaper and lock screen wallpaper — 144 bits and 2¹⁴⁴ combinations. The more combinations possible, the higher probability of generating a unique value suitable for use as an ID. And hence, it’s likely you can easily be tracked. &lt;/p&gt;

&lt;p&gt;2¹⁴⁴ = 22,300,745,198,530,623,141,535,718,272,648,361,505,980,416&lt;/p&gt;

&lt;p&gt;How large is this number, exactly? For context, the universe is made up of around 10⁸⁰ atoms. And 2¹⁴⁴ is approximately equal to 10⁴³. So the squared value of combinations is larger than the number of atoms in the universe! It’s safe to say that this outnumbers all devices on the Earth, for the foreseeable future. &lt;/p&gt;

&lt;h3&gt;
  
  
  The identification algorithm
&lt;/h3&gt;

&lt;p&gt;As you may recall, developers can use byte arrays to restore wallpaper images prior to Android 8.1. After version 8.1, 144 bits can be extracted from wallpaper colors. Both can be used as an ID, but let’s use them instead as inputs for the SHA-256 hash function (for unification).&lt;/p&gt;

&lt;p&gt;We have an ID that contains 256 bits, is unique across all applications, and only changes when the device wallpaper changes. The code for getting the ID is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;val id = hasher.hash(

   if (Build.VERSION.SDK_INT &amp;lt; Build.VERSION_CODES.O_MR1) {

       extractWallpaperBytes()

   } else extractColorsBytes())
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ID remains the same even after reinstalling the application and only changes when the wallpaper changes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it yourself
&lt;/h3&gt;

&lt;p&gt;For demonstration purposes, we’ve created an open source application that calculates the ID and checks its uniqueness. You can download the app on &lt;a href="https://play.google.com/store/apps/details?id=com.fingerprintjs.android.wallpaperid&amp;amp;hl=en_US&amp;amp;gl=US" rel="noopener noreferrer"&gt;Google Play&lt;/a&gt; (for Android 5.0 and above, no permissions are required); the source code is &lt;a href="https://github.com/fingerprintjs/android-wallpaper-id" rel="noopener noreferrer"&gt;available on GitHub&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; the method does not work on custom launchers that redefine logic of wallpaper management without using &lt;code&gt;WallpaperManager&lt;/code&gt; class.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhnf286weh7wksfrp625.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkhnf286weh7wksfrp625.png" alt="Open source wallpaper image application"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to prevent wallpaper tracking on your Android device
&lt;/h2&gt;

&lt;p&gt;The following measures can help prevent wallpaper tracking on your Android device:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Never use private or personal images for wallpapers, especially on devices running Android 8.1 and earlier. &lt;/li&gt;
&lt;li&gt;Use a default wallpaper and don’t change it. By using a custom image, you inadvertently add entropy and uniqueness for distinguishing your device from others.&lt;/li&gt;
&lt;li&gt;Check if your launcher has redefined the logic of the device’s wallpaper management (you can do this with our demo application).&lt;/li&gt;
&lt;li&gt;Don’t install suspicious applications. &lt;/li&gt;
&lt;li&gt;Be sure to keep your device operating system continuously updated.&lt;/li&gt;
&lt;li&gt;Use anti-malware software to ensure your installed applications are behaving as expected.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;As you can see, signals generated from wallpaper color extraction can be used to create a single identifier available to all applications, no additional permissions required. That said, extracting colors from device wallpapers is just one way mobile developers can uniquely profile Android devices, and it’s not the most dependable at that.&lt;/p&gt;

&lt;p&gt;For some examples of more stable and reliable methods, please view our  &lt;a href="https://github.com/fingerprintjs/fingerprint-android" rel="noopener noreferrer"&gt;fingerprint-android&lt;/a&gt; library source code. Google has not restricted these for a number of years now, and it is unlikely that it ever will. At the end of the day, doing so would impact Android’s efficacy as an advertising platform — and for the world’s largest tech firm, it’s a constant juggle between balancing these interests with protecting user privacy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get in touch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Star, follow, or fork our &lt;a href="https://github.com/fingerprintjs/fingerprint-android" rel="noopener noreferrer"&gt;production-grade library&lt;/a&gt; for Android device fingerprinting&lt;/li&gt;
&lt;li&gt;Email any questions you have to &lt;a href="//mailto:oss@fingerprintJS.com"&gt;oss@fingerprintJS.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Join our team and work on exciting research in device security: &lt;a href="//mailto:work@fingerprintjs.com"&gt;work@fingerprintjs.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>iOS 15 iCloud Private Relay Vulnerability Identified</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Mon, 20 Sep 2021 19:06:49 +0000</pubDate>
      <link>https://forem.com/savannahjs/ios-15-icloud-private-relay-vulnerability-identified-2fbi</link>
      <guid>https://forem.com/savannahjs/ios-15-icloud-private-relay-vulnerability-identified-2fbi</guid>
      <description>&lt;p&gt;&lt;em&gt;Apple’s new iCloud Private Relay service allows users to hide their IP addresses and DNS requests from websites and network service providers. In this article, we’ll demonstrate how this security feature can be circumvented and discuss what users can do to prevent their data from being leaked.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;You’ll need to turn on iCloud Private Relay to test the vulnerability. At the moment iCloud Private Relay is available only in Safari on iOS 15 for iCloud+ subscribers.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  &lt;a href="https://fingerprintjs.com/blog/ios15-icloud-private-relay-vulnerability/" rel="noopener noreferrer"&gt;Try the demo on our blog&lt;/a&gt;
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Please note that this leak only occurs with iCloud Private Relay on iOS 15—the vulnerability has been fixed in MacOS Monterey beta.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  IP addresses and online privacy
&lt;/h2&gt;

&lt;p&gt;Online privacy is a hot topic of debate these days. Internet users don’t like being tracked online; on the other hand, advertisers need to glean user behavior insights to display the most relevant (and profitable) ads. Unfortunately, this requires access to sensitive and private information that users may not be willing to share.&lt;/p&gt;

&lt;p&gt;Your IP address is one such piece of information used for &lt;a href="https://hal.inria.fr/hal-02435622/document" rel="noopener noreferrer"&gt;tracking your activity&lt;/a&gt; across websites. These numerical labels (e.g., &lt;code&gt;1.2.3.4&lt;/code&gt;) are relatively stable and unique even if they aren’t dedicated, and can be used to determine your location with building-level precision.&lt;/p&gt;

&lt;p&gt;Another way interested parties can track your online activity is to analyze your domain name system (DNS) requests. Like users and their devices, a website is also identified by its IP address. However, visitors need only type in the associated domain name (e.g., example.com) into the address bar, since DNS does the hard part of translating alphanumeric domain names into numeric IP addresses for proper routing.&lt;/p&gt;

&lt;p&gt;This of course makes the internet a lot more user friendly, but also creates another way for interested parties to track your online behavior. DNS requests contain the names of the websites you visit, so they can be used to see your browsing history and your interests. DNS requests are unencrypted unless DNS-over-HTTPS is implemented, allowing your internet service provider and other parties between you and the DNS server to see your unencrypted DNS requests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is iCloud Private Relay
&lt;/h2&gt;

&lt;p&gt;Browser vendors (most notably Apple) have in recent years made a concerted effort to improve the security and privacy of their users. iCloud Private Relay is Apple’s latest feature for protecting users against these online tracking techniques.&lt;/p&gt;

&lt;p&gt;According to &lt;a href="https://www.apple.com/" rel="noopener noreferrer"&gt;Apple’s website&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“iCloud Private Relay is a service that lets you connect to virtually any network and browse with Safari in an even more secure and private way. It ensures that the traffic leaving your device is encrypted and uses two separate internet relays so no one can use your IP address, location, and browsing activity to create a detailed profile about you.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The service works by proxying network/HTTP traffic (including DNS requests) from the Safari browser, as well as unencrypted HTTP traffic from applications. By doing this, Apple claims that network providers no longer can see your DNS requests and unencrypted HTTP traffic; similarly, websites visited will only see your iCloud-assigned proxy IP address.This address is drawn from a pool shared between multiple iCloud Private Relay users, grouped by their approximate location (Apple provides a &lt;a href="https://mask-api.icloud.com/egress-ip-ranges.csv" rel="noopener noreferrer"&gt;public table&lt;/a&gt; of proxy IPs/locations).&lt;/p&gt;

&lt;p&gt;The following diagram from Apple illustrates how iCloud Private Relay handles HTTPS requests and what each party sees:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnthk4i475ihbgqt1bn48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fnthk4i475ihbgqt1bn48.png" alt="iCloud Private Relay Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;iCloud Private Relay is available exclusively for iCloud+ subscribers running iOS 15 or macOS 12 Monterey with the Safari browser. Unfortunately, it’s unavailable in &lt;a href="https://www.reuters.com/world/china/apples-new-private-relay-feature-will-not-be-available-china-2021-06-07/" rel="noopener noreferrer"&gt;several countries&lt;/a&gt; due to regulatory limitations.&lt;/p&gt;

&lt;p&gt;You can learn more about iCloud Private Relay in &lt;a href="https://developer.apple.com/videos/play/wwdc2021/10096/" rel="noopener noreferrer"&gt;this video&lt;/a&gt; from Apple.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to get the real IP of a client that uses iCloud Private Relay
&lt;/h2&gt;

&lt;p&gt;If you read the IP address from an HTTP request received by your server, you’ll get the IP address of the egress proxy. Nevertheless, you can get the real client’s IP through WebRTC. The process is described in detail below.&lt;/p&gt;

&lt;p&gt;If you want to go straight to the leak explanation, skip the following “what is” sections.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is WebRTC
&lt;/h3&gt;

&lt;p&gt;WebRTC (web real-time communication) is a browser API for websites to establish direct communication between website visitors (i.e., peer-to-peer). The communication allows sending and receiving audio, video, and arbitrary data between browsers without requiring an intermediate server. All modern browsers support WebRTC natively; for example, Google Hangouts is one of the more popular applications that uses WebRTC — it works with all browsers and launches meetings with a click/tap, no plugin installation required.&lt;/p&gt;

&lt;p&gt;WebRTC is a complex API. More information is available from &lt;a href="https://codelabs.developers.google.com/codelabs/webrtc-web/" rel="noopener noreferrer"&gt;this Google guide&lt;/a&gt;; however, we will only cover the aspects required for the IP address leak.&lt;/p&gt;

&lt;p&gt;In order for browsers to communicate with each other, they first need to connect. This sounds simple, but in actuality is not a trivial task because— unlike servers—a website visitor’s device has no public IP to connect to. The ICE protocol was created to solve this problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is ICE
&lt;/h3&gt;

&lt;p&gt;ICE (interactive connectivity establishment) is a framework used by WebRTC. It lets two browsers find and connect with each other for direct, peer-to-peer communications.When one browser wants to connect to another, it will collect all possible hosts in a process called “collecting ICE candidates”. An ICE candidate is a piece of text that includes the host (IP address or domain name), port, protocol and other information. The browser will return the ICE candidates to the browser application.&lt;/p&gt;

&lt;p&gt;The following is an example of an ICE candidate (see the full format description in the &lt;a href="https://datatracker.ietf.org/doc/html/rfc5245#section-15.1" rel="noopener noreferrer"&gt;RFC document&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcjs1v300tc9yn6kurik.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fmcjs1v300tc9yn6kurik.png" alt="ICE candidate structure"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let’s say Alice wants to connect to Bob. Alice’s browser will collect all its ICE candidates and send them to Bob through the website’s server. This is the only time when a server is needed, any further communication will happen directly between Alice and Bob. When Bob receives Alice's ICE candidates from the server, it will try to connect to Alice using the addresses and ports from the list until it finds one that works.&lt;/p&gt;

&lt;p&gt;Different types of ICE candidates exist—for this demonstration, we’re concerned with the Server Reflexive Candidate. You can recognize it by a &lt;code&gt;typ srflx&lt;/code&gt; substring. It contains an IP address and a port from a STUN server that allows Bob to connect through Alice's NAT.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are NAT and STUN
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Network_address_translation" rel="noopener noreferrer"&gt;NAT&lt;/a&gt; (network address translation) is a protocol that allows multiple devices to connect to the internet using a single internet connection and public IP address. Every home router implements NAT—in fact, your device is most likely behind NAT now.&lt;/p&gt;

&lt;p&gt;Devices inside a network using NAT have no public IP addresses, so they can’t be accessed from the internet directly. The &lt;a href="https://en.wikipedia.org/wiki/STUN" rel="noopener noreferrer"&gt;STUN&lt;/a&gt; (session traversal utilities for NAT) protocol was created to solve this problem.&lt;/p&gt;

&lt;p&gt;A STUN server performs one small but critical task: it returns your public IP address and port number. We won’t cover NAT traversal in-depth (you can &lt;a href="https://tailscale.com/blog/how-nat-traversal-works/" rel="noopener noreferrer"&gt;learn more here&lt;/a&gt;); just know that Alice gets an ICE candidate containing her public IP address and port returned by the STUN server, and Bob can connect to her using this IP address and port.&lt;/p&gt;

&lt;p&gt;This is what happens when a WebRTC connection is established from behind a NAT:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3sfn8tc7qyyxczk95rfd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3sfn8tc7qyyxczk95rfd.png" alt="WebRTC connection diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;WebRTC requests two types of IP addresses from the STUN server: IPv4 and IPv6. If the STUN server and the user’s network supports IPv6, WebRTC will receive 2 ICE candidates: one with the IPv4 address and the other with the IPv6 address.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Leak
&lt;/h3&gt;

&lt;p&gt;Because Safari doesn’t proxy STUN requests through iCloud Private Relay, STUN servers know your real IP address. This isn’t an issue on its own, as they have no other information; however, Safari passes ICE candidates containing real IP addresses to the JavaScript environment. De-anonymizing you then becomes a matter of parsing your real IP address from the ICE candidates — something easily accomplished with a web application.&lt;/p&gt;

&lt;p&gt;So, in order to get real IP addresses, you need to create a peer connection object with a STUN server, collect the ICE candidates, and parse the values. This method requires no user permissions and works on both HTTP and HTTPS pages. Additionally, it’s fast (the time it takes for a couple of parallel network requests) and leaves no traces in browser development tools.&lt;/p&gt;

&lt;p&gt;First, create a peer connection object with at least one STUN server. We’ll use Google's server because it’s widely referenced in examples across the internet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;js
const peerConnection = new RTCPeerConnection({
  iceServers: [{
    urls: 'stun:stun.l.google.com:19302'
  }]
})
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Subscribe to the &lt;code&gt;icecandidate&lt;/code&gt; event to receive ICE candidates. There will be STUN candidates and candidates of other types, so you need to filter the STUN candidates and parse their IP addresses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ips&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;isSTUNCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; typ srflx &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;parseIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;peerConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onicecandidate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;candidateString&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;candidate&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;isSTUNCandidate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidateString&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;parseIP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;candidateString&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// There will be no other ICE candidates&lt;/span&gt;
    &lt;span class="c1"&gt;// Print the result&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IPs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ips&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, create a data channel and an offer to make WebRTC start collecting ICE candidates.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;peerConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDataChannel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;peerConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOffer&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;peerConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setLocalDescription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The process will complete when you receive an &lt;code&gt;icecandidate&lt;/code&gt; event with null candidate. The &lt;code&gt;ips&lt;/code&gt; array will contain your real IPs (IPv4 and IPv6 depending on your network connection). For the complete code, please visit &lt;a href="https://github.com/fingerprintjs/blog-private-relay-ip-leak-demo/blob/master/src/main_page.ts#L131" rel="noopener noreferrer"&gt;our GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to protect yourself from the leak
&lt;/h2&gt;

&lt;p&gt;Using a real VPN instead of iCloud Private Relay will proxy all your network traffic—including STUN requests and other browser traffic—so that no one except you and the VPN provider can see your real IP address. A myriad of VPN apps are available in the App Store.&lt;/p&gt;

&lt;p&gt;Disabling JavaScript in your Safari’s browser settings will turn off WebRTC and provide protection from this leak. However, many websites require Javascript to function properly.&lt;/p&gt;

&lt;p&gt;To fix this vulnerability, Apple will need to modify Safari so it routes all traffic through iCloud Private Relay. The FingerprintJS Team has already reported this issue to them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get in touch
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Try our &lt;a href="https://github.com/fingerprintjs/fingerprintjs" rel="noopener noreferrer"&gt;open source browser fingerprinting library&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Try &lt;a href="https://fingerprintjs.com/" rel="noopener noreferrer"&gt;FingerprintJS Pro&lt;/a&gt;, which combines browser fingerprinting with additional identification techniques and machine learning for 99.5% accuracy. It’s free for 10 days with unlimited API calls.&lt;/li&gt;
&lt;li&gt;Join our team to work on exciting research in online privacy and cybersecurity: &lt;a href="//mailto:work@fingerprintjs.com"&gt;work@fingerprintjs.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>opensource</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>How ad blockers can be used for browser fingerprinting</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Thu, 01 Jul 2021 21:03:44 +0000</pubDate>
      <link>https://forem.com/savannahjs/how-ad-blockers-can-be-used-for-browser-fingerprinting-5808</link>
      <guid>https://forem.com/savannahjs/how-ad-blockers-can-be-used-for-browser-fingerprinting-5808</guid>
      <description>&lt;p&gt;&lt;em&gt;In this article, we show how signals generated by the use of an ad blocker can improve browser fingerprinting accuracy. This novel browser fingerprinting method, while oft-discussed as a theoretical source of entropy, has only just been added to FingerprintJS as of April 2021, and has never been fully described until now. Ad blockers are an incredibly pervasive and useful piece of technology. Around 26% of Americans use an ad blocker today. If you are reading this article on ad blocker technology, you almost undoubtedly have one installed.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;While ad blockers make the internet a more pleasant experience for many people, whether or not they protect your privacy in any meaningful way is up for debate. As ad blockers have access to the content of all pages that a browser loads and can reliably perform cross-site tracking, they are able to collect more information on a user’s browsing activity than most marketing trackers they block.&lt;/p&gt;

&lt;p&gt;Perhaps more insidiously, the fact that a user is attempting to avoid being tracked online with an ad blocker can be used to identify them. Consider the example of tracking an individual in the woods by their shoe print. You may find success if you know their shoe’s size and ridge pattern, but it may be just as easy if you know that person habitually covers their tracks by raking a branch over their path. Whether you are looking for a shoe print or the absence of one, a signature pattern can be found.&lt;/p&gt;

&lt;p&gt;Ad blockers leave a trace that can be harnessed by the websites you visit to identify you. By testing whether certain page elements are blocked, a site can find discrepancies in the filters used by your specific ad blocker(s). These discrepancies provide a source of entropy that when combined with other unique signals, can identify a specific user over multiple visits. This combining of browser signals to create a unique identifier is known as browser fingerprinting. &lt;/p&gt;

&lt;p&gt;While browser fingerprinting is a proven-out method of visitor identification (you can read more about how it works in our beginner’s guide), how ad blockers can be used for fingerprinting is rarely discussed. As the developers of the largest open source browser fingerprinting library, we have only started including ad blocker signals as of April 2021, so this work is hot off the press from our team. We hope shining a light on this cutting edge technique will be useful to the open source community at large.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is an ad blocker
&lt;/h2&gt;

&lt;p&gt;An ad blocker is a browser extension that prevents browsers from loading video and displaying advertisements, pop-ups, tracking pixels and other third-party scripts. &lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2FnOBtmxeQ-nwYhOX1tRe0NF8mGfYdQHRnV-6G_3OXALdvGyq-fUR5DCcl-Pq_XnJsXcuzbbsYC-bmJkFQgk-JZQlp3GU0OwGGHskeDdT6dz-wgs91UCjwn40TDgaXX3msLiifBGAV" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh4.googleusercontent.com%2FnOBtmxeQ-nwYhOX1tRe0NF8mGfYdQHRnV-6G_3OXALdvGyq-fUR5DCcl-Pq_XnJsXcuzbbsYC-bmJkFQgk-JZQlp3GU0OwGGHskeDdT6dz-wgs91UCjwn40TDgaXX3msLiifBGAV" title="What is an ad blocker" alt="What is an ad blocker"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ad blockers not only improve the online experience by hiding ads, but also protect browsing activity from being tracked by third-party scripts. All major online ad platforms (like Google and Facebook), as well as other marketing and product testing tools (like Crazy Egg and Hotjar) use tracking scripts to monitor and monetize user activity online. Privacy conscious users often turn to ad blockers to stop their browsing history from being shared with these platforms. &lt;/p&gt;

&lt;p&gt;However, ad blockers have access to the content of all pages that a browser loads. They have a lot more information about browsing activity than trackers, because trackers can’t do reliable cross-site tracking. Therefore, &lt;a href="https://arstechnica.com/information-technology/2020/10/popular-chromium-ad-blockers-caught-stealing-user-data-and-accessing-accounts/" rel="noopener noreferrer"&gt;it is possible for ad blockers to violate user privacy&lt;/a&gt;.&lt;br&gt;
Safari is an exception which we’ll discuss below.&lt;/p&gt;
&lt;h2&gt;
  
  
  How ad blockers work
&lt;/h2&gt;

&lt;p&gt;In this section we’ll go fairly deep into the internals of ad blockers as it will help us build a better understanding of how ad blocking mechanics make it possible to reliably identify visitors. &lt;/p&gt;

&lt;p&gt;Ad blockers typically run as extensions built on top of browser APIs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://developer.chrome.com/docs/extensions/reference/" rel="noopener noreferrer"&gt;Google Chrome&lt;/a&gt; and other Chromium-based browsers: Extensions are JavaScript applications that run in a sandboxed environment with additional browser APIs available only to browser extensions.  There are two ways ad blockers can block content. The first one is element hiding and the second one is resource blocking:&lt;/li&gt;
&lt;li&gt;Element hiding is done either by injecting CSS code, or by using DOM APIs such as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/querySelectorAll" rel="noopener noreferrer"&gt;querySelectorAll&lt;/a&gt; or &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Node/removeChild" rel="noopener noreferrer"&gt;removeChild&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Resource blocking employs a different technique. Instead of rendering elements on a page and then hiding them, extensions block the resources on a browser networking level. To plug into browser networking, ad blockers will intercept requests as they happen or use declarative blocking rules defined beforehand. Request interception utilizes &lt;a href="https://developer.chrome.com/docs/extensions/reference/webRequest/" rel="noopener noreferrer"&gt;webRequest&lt;/a&gt; API, which is the most privacy violating technique. It works by reading every request that a browser is making and deciding on the fly if it represents an ad and should be blocked. The declarative approach utilizes &lt;a href="https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/" rel="noopener noreferrer"&gt;declarativeNetRequest&lt;/a&gt; API to preemptively instruct browsers what needs to be blocked. This happens without reading actual requests, thus providing more privacy.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions" rel="noopener noreferrer"&gt;Firefox&lt;/a&gt;: This API is almost the same as in Google Chrome. The only notable difference is the lack of &lt;a href="https://developer.chrome.com/docs/extensions/reference/declarativeNetRequest/" rel="noopener noreferrer"&gt;declarativeNetRequest&lt;/a&gt; API.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://developer.apple.com/documentation/safariservices/safari_web_extensions" rel="noopener noreferrer"&gt;Safari&lt;/a&gt;: Unlike Chrome or Firefox, Safari extensions are native applications. Safari provides &lt;a href="https://developer.apple.com/documentation/safariservices/creating_a_content_blocker" rel="noopener noreferrer"&gt;a declarative API&lt;/a&gt; for ad blockers. Ad blockers create static lists of things that describe what to block, and pass them to Safari. A list will contain rules that tell what network requests, HTML elements or cookies to block. A list content may also depend on user settings. Ad blockers have no way of accessing browsing history in Safari. You can watch &lt;a href="https://developer.apple.com/videos/play/wwdc2015/511/" rel="noopener noreferrer"&gt;a video by Apple with a detailed explanation&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Android browsers are a special case, in that they generally lack extension APIs. However, Android Play Market allows you to install ad-blocking apps that will work in all browsers. These apps will create a VPN on the system level and pass all the device traffic through it. The VPN connection will act as an ad blocker by adding JavaScript code or CSS styles to pages that will hide unwanted content, or by blocking HTTP requests entirely.&lt;/p&gt;
&lt;h3&gt;
  
  
  Ad blocking filters
&lt;/h3&gt;

&lt;p&gt;Ad blockers prevent ads from being shown by looking for specific elements to block within the site’s contents. To identify these advertising elements, ad blockers use collections of rules called "filters" to decide what to block.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2Fw4qO5U7HQl2sBCMJE6gnuhCQLbOE0UW8I0a_cRKekJ2AK-A-YjA_csDCPFtWMb0KRILbmoHEUO2XEHcs0go6v6fgfVHUzcBMds7MFScZfot1e6B4ccaykOjf2vC4lTlZG9N6Dpzc" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2Fw4qO5U7HQl2sBCMJE6gnuhCQLbOE0UW8I0a_cRKekJ2AK-A-YjA_csDCPFtWMb0KRILbmoHEUO2XEHcs0go6v6fgfVHUzcBMds7MFScZfot1e6B4ccaykOjf2vC4lTlZG9N6Dpzc" title="How ad blocker filters work" alt="How ad blocker filters work"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Usually these filters are maintained by the open source community. Like any other project, filters are created by different people for different needs. For example, French websites often use local ad systems that are not known worldwide and are not blocked by general ad filters, so developers in France will want to create a filter to block ads on French websites. Some filter maintainers can have privacy concerns and thus create filters that block trackers.&lt;/p&gt;

&lt;p&gt;A filter is usually a text file that follows a common standard called "&lt;a href="https://help.eyeo.com/en/adblockplus/how-to-write-filters" rel="noopener noreferrer"&gt;AdBlock Plus syntax&lt;/a&gt;". Each line of text contains a blocking rule, which tells an ad blocker which URLs or CSS selectors must be blocked. Each blocking rule can have additional parameters such as the domain name or the resource type. &lt;/p&gt;

&lt;p&gt;A blocking rule example is shown below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2F54P_Mlf61BTLLYMCsYn3Sd35mrIXHTymQrXvnS09ZKA0r-CahOU2n2dwqMhVau1gkqPByS2PpGRQmylrgBe16pV6i1KRjts_fII7_OB5_UUiDq4G-DivaKdiB3Biw3vCINPWwOK9" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Flh5.googleusercontent.com%2F54P_Mlf61BTLLYMCsYn3Sd35mrIXHTymQrXvnS09ZKA0r-CahOU2n2dwqMhVau1gkqPByS2PpGRQmylrgBe16pV6i1KRjts_fII7_OB5_UUiDq4G-DivaKdiB3Biw3vCINPWwOK9" title="Ad blocker blocking rule" alt="Ad blocker blocking rule"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most common sets of filters used by AdBlock, AdGuard and other ad blockers include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://easylist.to/" rel="noopener noreferrer"&gt;EasyList&lt;/a&gt;: includes &lt;a href="https://easylist.to/easylist/easylist.txt" rel="noopener noreferrer"&gt;EasyList&lt;/a&gt;, &lt;a href="https://easylist.to/easylist/easyprivacy.txt" rel="noopener noreferrer"&gt;EasyPrivacy&lt;/a&gt;, &lt;a href="https://secure.fanboy.co.nz/fanboy-cookiemonster.txt" rel="noopener noreferrer"&gt;EasyList Cookie List&lt;/a&gt;, &lt;a href="https://easylist.to/easylistgermany/easylistgermany.txt" rel="noopener noreferrer"&gt;EasyList Germany&lt;/a&gt; and many others.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kb.adguard.com/en/general/adguard-ad-filters#adguard-filters" rel="noopener noreferrer"&gt;AdGuard&lt;/a&gt;: includes a &lt;a href="https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_2_English/filter.txt" rel="noopener noreferrer"&gt;base filter&lt;/a&gt;, a &lt;a href="https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_11_Mobile/filter.txt" rel="noopener noreferrer"&gt;mobile ads filter&lt;/a&gt;, a &lt;a href="https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_3_Spyware/filter.txt" rel="noopener noreferrer"&gt;tracking protection filter&lt;/a&gt;, a &lt;a href="https://raw.githubusercontent.com/AdguardTeam/FiltersRegistry/master/filters/filter_4_Social/filter.txt" rel="noopener noreferrer"&gt;social media filter&lt;/a&gt; and many others.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.fanboy.co.nz/" rel="noopener noreferrer"&gt;Fanboy&lt;/a&gt;: includes &lt;a href="https://secure.fanboy.co.nz/enhancedstats.txt" rel="noopener noreferrer"&gt;Enhanced Trackers List&lt;/a&gt;, &lt;a href="https://www.fanboy.co.nz/fanboy-antifacebook.txt" rel="noopener noreferrer"&gt;Anti-Facebook Filters&lt;/a&gt;, &lt;a href="https://secure.fanboy.co.nz/fanboy-annoyance.txt" rel="noopener noreferrer"&gt;Annoyance List&lt;/a&gt; and several others.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  How to get entropy from ad blockers
&lt;/h2&gt;

&lt;p&gt;Our goal is to get as much information from ad blockers as possible to generate a fingerprint.&lt;/p&gt;

&lt;p&gt;A JS script running on a page can't tell directly if the browser has an ad blocker, and if it does, what is blocked by it. Instead, the script can try adding something on the page to see if it gets blocked. The addition can be an HTML element that matches a blocked CSS selector or an external resource such as a script or an image.&lt;/p&gt;

&lt;p&gt;We recommend using CSS selectors over resources to detect ad blockers, as resource detection has two significant downsides. Firstly, detecting whether a resource is blocked requires trying to download the resource by making an HTTPS request and watching its state. This process slows down the web page by occupying the network bandwidth and CPU. Secondly, the HTTPS requests will appear in the browser developer tools, which may look suspicious to an observant site visitor. For these reasons, we will focus on using CSS selectors to collect data in this article.&lt;/p&gt;

&lt;p&gt;We will now run through how to generate two related data sources using ad blocker signals: the list of blocked CSS selectors, and the list of filters. Finding the list of filters will result in a significantly more stable fingerprint, but requires additional work to identify unique CSS selectors to distinguish each filter from one another.&lt;/p&gt;
&lt;h3&gt;
  
  
  Data source 1: detecting the list of blocked CSS selectors
&lt;/h3&gt;

&lt;p&gt;The process of detecting whether a CSS selector is blocked consists of the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse the selector, i.e. get the tag name, CSS classes, id and attributes from it;&lt;/li&gt;
&lt;li&gt;Create an empty HTML element that matches that selector and insert the element into the document;&lt;/li&gt;
&lt;li&gt;Wait for the element to be hidden by an ad blocker, if one is installed;&lt;/li&gt;
&lt;li&gt;Check whether it's hidden. One way to do it is checking the element's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent" rel="noopener noreferrer"&gt;offsetParent&lt;/a&gt; property (it's null when the element is hidden).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do the above steps for each selector, you'll face a &lt;a href="https://developers.google.com/web/fundamentals/performance/rendering/avoid-large-complex-layouts-and-layout-thrashing#avoid_layout_thrashing" rel="noopener noreferrer"&gt;performance issue&lt;/a&gt;, because there will be a lot of selectors to check. To avoid slowing down your web page, you should create all the HTML elements first and then check them to determine if they are hidden.&lt;/p&gt;

&lt;p&gt;This approach can generate false positives when there are a lot of HTML elements added to the page. It happens because some CSS selectors apply only when an element has certain siblings. Such selectors contain a general sibling combinator (~) or an adjacent sibling combinator (+). They can lead to false element hiding and therefore false blocked selector detection results. This problem can be mitigated by inserting every element into an individual &amp;lt; div&amp;gt; container so that each element has no siblings. This solution may still fail occasionally, but it reduces the false positives significantly.&lt;/p&gt;

&lt;p&gt;Here is an example code that checks which selectors are blocked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getBlockedSelectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A storage for the test elements&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;elements&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allSelectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockedSelectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// First create all elements that can be blocked&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;allSelectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;div&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;selectorToElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allSelectors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt;
      &lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// Then wait for the ad blocker to hide the element&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;// Then check which of the elements are blocked&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;allSelectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;offsetParent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;blockedSelectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allSelectors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Then remove the elements&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;element&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;elements&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parentNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blockedSelectors&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Creates a DOM element that matches the given selector&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;selectorToElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// See the implementation at https://bit.ly/3yg1zhX&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;getBlockedSelectors&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.advertisement&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;img[alt="Promo"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blockedSelectors&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blockedSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To determine which CSS selectors to check, you can download some of &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/f1174cf83e2ec94d0c576d4caabf9ebbcf41fccc/docs/content_blockers.md#list-of-filters" rel="noopener noreferrer"&gt;the most popular filters&lt;/a&gt; and extract the CSS selectors that are blocked on all websites. The rules for such selectors start with ##.&lt;/p&gt;

&lt;p&gt;Your chosen selectors should contain no &amp;lt; embed&amp;gt;, no fixed positioning, no pseudo classes and no combinators. The offsetParent check will not work with either &amp;lt; embed&amp;gt; or fixed positioning. Selectors with combinators require a sophisticated script for building test HTML elements, and since there are only a few selectors with combinators, it isn't worth writing such a script. Finally, you should test only unique selectors across all the filters to avoid duplicate work. You can see a script that we use to parse the unique selectors from the filters &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/f1174cf83e2ec94d0c576d4caabf9ebbcf41fccc/resources/content_blocking/make_selectors_tester.ts" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You can see some of the selectors blocked by your browser in the &lt;a href="https://fingerprintjs.com/blog/ad-blocker-fingerprinting/" rel="noopener noreferrer"&gt;interactive demo&lt;/a&gt; on our blog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3j3j8y61xvrnd4esd0x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw3j3j8y61xvrnd4esd0x.png" alt="Interactive demo"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;This is just an image - check out the full interactive demo on &lt;a href="https://fingerprintjs.com/blog/ad-blocker-fingerprinting/" rel="noopener noreferrer"&gt;our site!&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Data source 2: getting the list of ad blocking filters
&lt;/h3&gt;

&lt;p&gt;A better way to get identification entropy from ad blockers is detecting which filters an ad blocker uses. This is done by identifying unique CSS selectors for each filter, so that if a unique selector is blocked, you can be sure a visitor is using that filter.&lt;/p&gt;

&lt;p&gt;The process consists of the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify which selectors are blocked by each filter. This step will be done once as a preparation step.&lt;/li&gt;
&lt;li&gt;Get unique selectors by filter. This step will also be done once as a preparation step. &lt;/li&gt;
&lt;li&gt;Check whether each unique selector is blocked. This step will run in the browser every time you need to identify a visitor.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These three steps are explained in more detail below.&lt;/p&gt;
&lt;h4&gt;
  
  
  Identify which selectors are blocked by each filter
&lt;/h4&gt;

&lt;p&gt;To get the selectors blocked by a filter we can’t just read them from the filter file. This approach will not work in practice because ad blockers can hide elements differently from filter rules. So, to get a true list of CSS selectors blocked by a filter, we need to use a real ad blocker.&lt;/p&gt;

&lt;p&gt;The process of detecting which selectors a filter really blocks is described next:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make an HTML page that checks every selector from the filters you want to detect. The page should use the process described in the previous section (detecting the list of blocked CSS selectors). You can use &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/f1174cf83e2ec94d0c576d4caabf9ebbcf41fccc/resources/content_blocking/make_selectors_tester.ts" rel="noopener noreferrer"&gt;a Node.js script&lt;/a&gt; that makes such an HTML page. This step will be done once as a preparation step.&lt;/li&gt;
&lt;li&gt;Go to the ad blocker settings and enable only the filter we’re testing;&lt;/li&gt;
&lt;li&gt;Go to the HTML page and reload it;&lt;/li&gt;
&lt;li&gt;Save the list of blocked selectors to a new file.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Repeat the steps for each of the filters. You will get a collection of files (one for each filter).&lt;/p&gt;

&lt;p&gt;Some filters will have no selectors, we won’t be able to detect them.&lt;/p&gt;
&lt;h4&gt;
  
  
  Get unique selectors by filter
&lt;/h4&gt;

&lt;p&gt;Now, when you have selectors that are really blocked by each of the filters, we can narrow them down to the unique ones. A unique selector is a selector that is blocked by only one filter. We created &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/f1174cf83e2ec94d0c576d4caabf9ebbcf41fccc/resources/content_blocking/get_unique_filter_selectors.ts" rel="noopener noreferrer"&gt;a script&lt;/a&gt; that extracts unique selectors. The script output is a JSON file that contains unique blocked selectors for each of the filters.&lt;/p&gt;

&lt;p&gt;Unfortunately, some of the filters have no unique blocked selectors. They are fully included into other filters. That is, all their rules are presented in other filters, thus making these rules not unique.&lt;/p&gt;

&lt;p&gt;You can see how we handle such filters in our &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/f1174cf83e2ec94d0c576d4caabf9ebbcf41fccc/docs/content_blockers.md#5-handle-empty-filters" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;
  
  
  Identify blocked selectors by filter
&lt;/h4&gt;

&lt;p&gt;This part will run in the browser. In a perfect world we would only need to check whether a single selector from each of the filters is blocked. When a unique selector is blocked, you can be sure that the person uses the filter. Likewise, if a unique selector isn't blocked, you can be sure the person doesn't use the filter.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uniqueSelectorsOfFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;easyList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[lazy-ad="leftthin_banner"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fanboyAnnoyances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#feedback-tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getActiveFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectorArray&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="c1"&gt;// See the snippet above&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockedSelectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlockedSelectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selectorArray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filterName&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filterName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blockedSelectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;getActiveFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectorsOfFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeFilters&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In practice, the result may sometimes be incorrect because of wrong detection of blocked selectors. It can happen for several reasons: ad blockers can update their filters, they can experience glitches, or page CSS can interfere with the process.&lt;/p&gt;

&lt;p&gt;In order to mitigate the impact of unexpected behavior, we can use fuzzy logic. For example, if more than 50% of unique selectors associated with one filter are blocked, we will assume the filter is enabled. An example code that checks which of the given filters are enabled using a fuzzy logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uniqueSelectorsOfFilters&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;easyList&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[lazy-ad="leftthin_banner"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ad_300x250_2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;fanboyAnnoyances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#feedback-tab&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#taboola-below-article&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getActiveFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Collect all the selectors into a plain array&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;allSelectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[].&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blockedSelectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getBlockedSelectors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filterName&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selectors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uniqueSelectors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;filterName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;blockedSelectorCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

      &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;selector&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blockedSelectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;selector&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;blockedSelectorCount&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;blockedSelectorCount&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;selectors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;getActiveFilters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;uniqueSelectorsOfFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeFilters&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Ad blocker fingerprinting
&lt;/h2&gt;

&lt;p&gt;Once you collect enough data, you can generate a visitor fingerprint.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://fingerprintjs.com/blog/what-is-browser-fingerprinting/" rel="noopener noreferrer"&gt;Browser fingerprinting&lt;/a&gt; is a technique that works by reading browser attributes and combining them together into a single identifier. This identifier is stateless and works well in normal and incognito modes.&lt;/p&gt;

&lt;p&gt;There are dozens of ad blockers available. For example, AdBlock, uBlock Origin, AdGuard, 1Blocker X. These ad blockers use different sets of filters by default. Also users can customize ad blocking extensions by removing default filters and adding custom filters. This diversity gives entropy that can be used to generate fingerprints and identify visitors.&lt;/p&gt;

&lt;p&gt;An example of an ad blocker customization:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhn5ssokaf14o28zj75z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhhn5ssokaf14o28zj75z.png" alt="Ad blocker settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A good browser fingerprint should stay the same when a user goes from regular to incognito (private) mode of the browser. Thus, ad blockers can provide a useful source of entropy only for browsers and operating systems where ad blockers are enabled by default in incognito mode:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Safari on MacOS, iOS, iPadOS:  browser extensions are enabled (including ad blockers) in both regular and incognito mode.&lt;/li&gt;
&lt;li&gt;All Browsers on Android: Ad blockers work on the system level, so they affect all browser modes. &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Desktop Chrome and Firefox:&lt;br&gt;
Extensions are disabled by default in incognito mode. Users however can manually choose to keep extensions enabled in incognito mode, but few people do so. Since we cannot know if a user has an ad blocker enabled in incognito mode, it makes sense to identify visitors by their ad blockers only in Safari and on Android.&lt;/p&gt;

&lt;p&gt;You can make a fingerprint solely from the information that we’ve gotten from the visitor's ad blocker either by using the list of blocked selectors, or the list of filters from the sections above.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using Selectors
&lt;/h3&gt;

&lt;p&gt;To make a fingerprint using selectors only, we take a list of selectors, check which of them are blocked and hash the result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// See the snippet above&lt;/span&gt;
&lt;span class="nf"&gt;getBlockedSelectors&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blockedSelectors&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// See the murmurHash3 implementation at&lt;/span&gt;
    &lt;span class="c1"&gt;// https://github.com/karanlyons/murmurHash3.js&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;murmurHash3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x86&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash128&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blockedSelectors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fingerprint is very sensitive but not stable. The CSS code of the page can accidentally hide a test HTML element and thus change the result. Also, as the community updates the filters quite often, every small update can add or remove a CSS selector rule, which will change the whole fingerprint. So, a fingerprint based on selectors alone can only be used for short-term identification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Using Filter Lists
&lt;/h3&gt;

&lt;p&gt;To mitigate the instability of CSS selectors alone, you can use the list of filters instead to generate a fingerprint. The list of filters that a person uses is only likely to change if they switch ad blockers, or if their installed ad blocker undergoes a significant update. To make a fingerprint, get the list of enabled filters and hash it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// See the snippet above&lt;/span&gt;
&lt;span class="nf"&gt;getActiveFilters&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeFilters&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// See the murmurHash3 implementation at&lt;/span&gt;
  &lt;span class="c1"&gt;// https://github.com/karanlyons/murmurHash3.js&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;murmurHash3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x86&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hash128&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;activeFilters&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyymq0iplbbiprp4c7cko.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyymq0iplbbiprp4c7cko.png" alt="Demo of filter lists"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;This is just an image - check out the full interactive demo on &lt;a href="https://fingerprintjs.com/blog/ad-blocker-fingerprinting/" rel="noopener noreferrer"&gt;our site!&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;As we mentioned above, the filter lists themselves are updated frequently. The updates can make the fingerprint change. The fuzzy algorithm mitigates this problem, but the underlying selectors will need to be updated eventually. So, you will need to repeat the process of collecting unique selectors after some time to actualize the data and keep the fingerprinting accuracy high.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;p&gt;The browser main thread is where it processes user events and paints. By default, browsers use a single thread to run all the JavaScript in the page, and to perform layout, reflows, and garbage collection. This means that long-running JavaScript can block the thread, leading to an unresponsive page and bad user experience.&lt;/p&gt;

&lt;p&gt;The process of checking CSS selectors runs on the main thread. The algorithm uses many DOM operations, such as &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Document/createElement" rel="noopener noreferrer"&gt;createElement&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent" rel="noopener noreferrer"&gt;offsetParent&lt;/a&gt;. These operations can run only on the main thread and can't be moved to a worker. So, it's important for the algorithm to run fast.&lt;/p&gt;

&lt;p&gt;We've measured the time it takes several old devices to check different numbers of CSS selectors per filter. We test only in the browsers where it makes sense to identify visitors by ad blockers. The tests were conducted in cold browsers on a complex page (about 500 KB of uncompressed CSS code). The results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;MacBook Pro 2015 (Core i7), macOS 11, Safari 14&lt;/th&gt;
&lt;th&gt;iPhone SE1, iOS 13, Safari 13&lt;/th&gt;
&lt;th&gt;Pixel 2, Android 9, Chrome 89&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1 selector per filter (45 in total)&lt;/td&gt;
&lt;td&gt;3.1ms&lt;/td&gt;
&lt;td&gt;10ms&lt;/td&gt;
&lt;td&gt;5.7ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At most 5 selectors per filter (210 in total)&lt;/td&gt;
&lt;td&gt;9ms&lt;/td&gt;
&lt;td&gt;27ms&lt;/td&gt;
&lt;td&gt;17ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;At most 10 selectors per filter (401 in total&lt;/td&gt;
&lt;td&gt;20ms&lt;/td&gt;
&lt;td&gt;20ms&lt;/td&gt;
&lt;td&gt;36ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All selectors (23029 in total)&lt;/td&gt;
&lt;td&gt;≈7000ms&lt;/td&gt;
&lt;td&gt;≈19000ms&lt;/td&gt;
&lt;td&gt;≈2600ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The more CSS selectors the algorithm checks, the more accurate the result will be. But a large number of CSS selectors increases the execution time and the code size. We have chosen to check 5 selectors per filter as a good balance between performance, stability and the code size.&lt;/p&gt;

&lt;p&gt;You can see a complete implementation of the described algorithm in &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/f1174cf83e2ec94d0c576d4caabf9ebbcf41fccc/src/sources/dom_blockers.ts" rel="noopener noreferrer"&gt;our GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Brave and Tor
&lt;/h2&gt;

&lt;p&gt;Brave is a browser based on Chromium. It disables extensions in incognito mode by default. Thus, we don't perform ad blocker fingerprinting in Brave.&lt;/p&gt;

&lt;p&gt;Desktop Tor has no separate incognito mode, so every extension works in all Tor tabs. Ad blockers can be used to identify Tor users. But the Tor authors &lt;a href="https://support.torproject.org/tbb/tbb-14/" rel="noopener noreferrer"&gt;strongly recommend&lt;/a&gt; not to install any custom extensions, and it's not easy to do so. Very few people will install ad blockers in Tor. So the effectiveness of ad blocker fingerprinting is low.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing thoughts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Ad blocker fingerprinting is only a small part of the larger identification process
&lt;/h3&gt;

&lt;p&gt;Ad blocker fingerprinting is one of the many signals our &lt;a href="https://github.com/fingerprintjs/fingerprintjs" rel="noopener noreferrer"&gt;open source library&lt;/a&gt; uses to generate a browser fingerprint. However, we do not blindly incorporate every signal available in the browser. Instead we analyze the stability and uniqueness of each signal separately to determine their impact on fingerprint accuracy.&lt;/p&gt;

&lt;p&gt;Ad blocker detection is a new signal and we’re still evaluating its properties.&lt;/p&gt;

&lt;p&gt;You can learn more about stability, uniqueness and accuracy in our &lt;a href="https://fingerprintjs.com/blog/what-is-browser-fingerprinting/" rel="noopener noreferrer"&gt;beginner’s guide to browser fingerprinting.&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Try Browser Fingerprinting for Yourself
&lt;/h3&gt;

&lt;p&gt;Browser fingerprinting is a useful method of visitor identification for a variety of anti-fraud applications. It is particularly useful to identify malicious visitors attempting to circumvent tracking by clearing cookies, browsing in incognito mode or using a VPN. &lt;/p&gt;

&lt;p&gt;You can try implementing browser fingerprinting yourself with our &lt;a href="https://github.com/fingerprintjs/fingerprintjs" rel="noopener noreferrer"&gt;open source library&lt;/a&gt;. FingerprintJS is the most popular browser fingerprinting library available, with over 14K GitHub stars.&lt;/p&gt;

&lt;p&gt;For higher identification accuracy, we also developed the &lt;a href="https://fingerprintjs.com/" rel="noopener noreferrer"&gt;FingerprintJS Pro API&lt;/a&gt;, which uses machine learning to combine browser fingerprinting with additional identification techniques. You can use FingerprintJS Pro for free with up to 20k API calls per month.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get in touch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Star, follow or fork our &lt;a href="https://github.com/fingerprintjs/fingerprintjs" rel="noopener noreferrer"&gt;GitHub project&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email us your questions at &lt;a href="mailto:oss@fingerprintJS.com"&gt;oss@fingerprintJS.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sign up to our &lt;a href="https://mailchi.mp/708d84efc0c1/updates-signup" rel="noopener noreferrer"&gt;newsletter&lt;/a&gt; for updates&lt;/li&gt;
&lt;li&gt;Join our team to work on exciting research in online security: &lt;a href="mailto:work@fingerprintjs.com"&gt;work@fingerprintjs.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Have Recent Browser Privacy Updates Stopped Browser Fingerprinting? An Analysis</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Tue, 25 May 2021 21:20:20 +0000</pubDate>
      <link>https://forem.com/savannahjs/have-recent-browser-privacy-updates-stopped-browser-fingerprinting-an-analysis-2nh9</link>
      <guid>https://forem.com/savannahjs/have-recent-browser-privacy-updates-stopped-browser-fingerprinting-an-analysis-2nh9</guid>
      <description>&lt;p&gt;The trend in web browsers over the past few years has generally been in favor of more privacy for users. Almost all mainstream browsers (&lt;a href="https://www.infoq.com/news/2020/04/safari-third-party-cookies-block/"&gt;Safari&lt;/a&gt;, &lt;a href="https://blog.mozilla.org/blog/2021/02/23/latest-firefox-release-includes-multiple-picture-in-picture-and-total-cookie-protection/"&gt;Firefox&lt;/a&gt;, &lt;a href="https://support.brave.com/hc/en-us/articles/360050634931-How-Do-I-Manage-Cookies-In-Brave-"&gt;Brave&lt;/a&gt;, and &lt;a href="https://blog.chromium.org/2020/01/building-more-private-web-path-towards.html"&gt;Chrome&lt;/a&gt;) now block third-party cookies which enabled tracking across multiple sites, and Chrome uses encrypted traffic (via HTTPS) by default.&lt;/p&gt;

&lt;p&gt;Similarly, regulations like GDPR and CCPA are adding legal hurdles for companies that want to gather user data online. Cookie consent boxes are now ubiquitous (despite the fact that &lt;a href="https://www.zdnet.com/article/cookie-consent-most-websites-break-law-by-making-it-hard-to-reject-all-tracking/"&gt;they're rarely implemented correctly&lt;/a&gt;), and companies in many jurisdictions are now responsible for allowing users the &lt;a href="https://en.wikipedia.org/wiki/Right_to_be_forgotten"&gt;right to be forgotten&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;More privacy is generally a good thing for legitimate users. It means fewer opportunities for their personal data to be stolen, it can decrease the risk of identity theft, and it gives them more control over how their data can be used.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;But, increased browser privacy also comes with a hidden cost. By preventing cookies and other tracking mechanisms, it's very hard for web application developers to figure out whether a visitor is a real user or a fraudulent bot.&lt;/strong&gt; This leads developers to rely heavily on multi-factor authentication methods, which slow down and frustrate users. It also increases the cost of building and maintaining applications - costs likely passed on to users in one form or another.&lt;/p&gt;

&lt;p&gt;There is another way to identify users without intrusive cookies. In this article, I'll share a technique called &lt;em&gt;browser fingerprinting&lt;/em&gt; and discuss how it can be used to prevent fraud in modern browsers. I'll also share an overview of the privacy settings that impact fingerprinting in each of the three major web browsers (Chrome, Safari, and Firefox). Finally, I'll share how fingerprinting must continue to evolve as privacy measures do.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Browser Fingerprinting Works
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://fingerprintjs.com/blog/what-is-browser-fingerprinting/"&gt;Browser fingerprinting&lt;/a&gt; can be used to uniquely identify users and their associate sessions regardless of anonymizing tactics like incognito browsing, VPNs, and cookie blockers. Unlike third-party cookies, which can be cleared or blocked by the browser, your browser fingerprint cannot be altered.&lt;/p&gt;

&lt;p&gt;Fingerprinting can be used to identify visitors with a pattern of fraudulent behavior and then target only these visitors for additional security checks. This means you won't slow down legitimate users who want to access your site, but you will be able to identify those who are trying to brute force access or circumvent your security measures.&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding Fraudulent Users
&lt;/h3&gt;

&lt;p&gt;Most fraudsters use identity concealing techniques like incognito mode or VPNs (virtual private networks) to hide their identity. This allows them to try multiple passwords, stolen credit cards, or email addresses in your app in an attempt to gain access to restricted data or make an illegitimate purchase.&lt;/p&gt;

&lt;p&gt;Fingerprinting takes in data from the browser to help build a unique profile for each user on your site. By capturing the following specifics, fingerprinting software can identify suspicious traffic without an IP address or cookie:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Computer make and model&lt;/li&gt;
&lt;li&gt;Operating system version&lt;/li&gt;
&lt;li&gt;Browser version&lt;/li&gt;
&lt;li&gt;Browser extensions&lt;/li&gt;
&lt;li&gt;Timezone&lt;/li&gt;
&lt;li&gt;Language settings&lt;/li&gt;
&lt;li&gt;Adblocker used&lt;/li&gt;
&lt;li&gt;Screen size and resolution&lt;/li&gt;
&lt;li&gt;Tech specs (CPU, GPU, hard disk, etc.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This method is surprisingly accurate when done well. &lt;strong&gt;&lt;a href="https://fingerprintjs.com/"&gt;FingerprintJS&lt;/a&gt; is 99.5% accurate&lt;/strong&gt; at identifying users and assigning them a unique visitorID. Using this ID, you can associate patterns of fraud with specific visitors and block them as needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Privacy in the Modern Browser
&lt;/h2&gt;

&lt;p&gt;As you might imagine, browser fingerprinting has had to evolve as browsers have evolved. In the past few decades, the laws and standards around online privacy, as well as the tactics available to web developers have changed a lot.&lt;/p&gt;

&lt;p&gt;For example, cookies on their own have always been relatively easy to bypass. Because they're stored on the user's browser, they can be easily cleared manually or programmatically, so they don't provide a useful mechanism for identifying malicious users.&lt;/p&gt;

&lt;p&gt;Other browser features are much harder to bypass. Since the introduction of &lt;a href="https://en.wikipedia.org/wiki/WebGL#Software"&gt;WebGL in the 2010s&lt;/a&gt;, web applications can draw a Canvas element onto the DOM. This uses the computer's graphics card to render an image that can be converted to a unique identifier for a user. Of course, browser extensions can block this method, so it's rarely used alone but as part of a comprehensive fingerprinting method.&lt;/p&gt;

&lt;p&gt;This continued arms race between companies that rely on tracking to prevent fraud and malicious users has led to a number of new features that affect fingerprinting in modern browsers. With extensions and native features in all of the top three browsers that can block or bypass some part of the fingerprinting process, let's look at some of the privacy settings that impact fingerprinting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser Fingerprinting in Chrome
&lt;/h3&gt;

&lt;p&gt;Chrome is currently the most popular browser, with a &lt;a href="https://gs.statcounter.com/browser-market-share#monthly-202103-202103-bar"&gt;64.19% market share&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--QsaoYQS0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ee6hg260jnbtgr4sj80w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--QsaoYQS0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ee6hg260jnbtgr4sj80w.png" alt="Current market share of modern browsers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Despite Google’s &lt;a href="https://www.cnet.com/how-to/google-collects-a-frightening-amount-of-data-about-you-you-can-find-and-delete-it-now/"&gt;spotty record of offering privacy to users&lt;/a&gt;, the Chrome browser does provide options that make tracking harder. First, users can block third-party cookies or all cookies or manually clear their cookies each time they close their browser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--G4QWJTCL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hoqbu1tsc5346e8qkmsk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--G4QWJTCL--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hoqbu1tsc5346e8qkmsk.png" alt="Google Chrome cookie settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While cookies may not be critical in most fingerprinting algorithms, Chrome is also currently running a trial of Google’s &lt;a href="https://www.privacysandbox.com/"&gt;Privacy Sandbox&lt;/a&gt;. This feature attempts to prevent fingerprinting by hiding your hardware and software information from websites. It’s not clear when these features will be on for all Chrome users, but that seems to be the direction Google is heading.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--7gj-57fX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4jxhp6sivjj22w3se2rc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--7gj-57fX--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4jxhp6sivjj22w3se2rc.png" alt="Chrome Privacy sandbox"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chrome also offers users the option to block access to operating system services or make websites ask before accessing them. Many of these services can be used to help fingerprint users if they’re enabled. Users can also turn off JavaScript completely, but this likely isn’t practical for real users as &lt;a href="https://w3techs.com/technologies/details/cp-javascript"&gt;most websites rely on JavaScript to function&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LTb2TsKY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wo9ljaap8pl03xn9r38c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LTb2TsKY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wo9ljaap8pl03xn9r38c.png" alt="Chrome permissions and content"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Chrome users can also use extensions to &lt;a href="https://chrome.google.com/webstore/search/fingerprint"&gt;block fingerprints&lt;/a&gt;, &lt;a href="https://chrome.google.com/webstore/search/vpn"&gt;obscure their IP addresses&lt;/a&gt;, and more. Each of these extensions limits tracking in its own way (for example, the &lt;a href="https://chrome.google.com/webstore/detail/canvas-fingerprint-defend/lanfdkkpgfjfdikkncbnojekcppdebfp"&gt;Canvas Fingerprint Detector&lt;/a&gt; blocks the HTML Canvas fingerprint method described above).&lt;/p&gt;

&lt;p&gt;Of course, no browser's privacy protections are perfect, and researchers are constantly finding new ways to track users in Chrome. For example, according to &lt;a href="https://arstechnica.com/information-technology/2020/03/study-ranks-edges-default-privacy-settings-the-lowest-of-all-major-browsers/"&gt;a 2020 paper by computer scientist Doug Leith&lt;/a&gt;, Chrome sends a "persistent identifier" as a header on each web request (presumably for debugging) that can be used as part of a browser fingerprint. It’s almost impossible for malicious users to completely avoid detection if fingerprinters are willing to stay up-to-date on the latest changes, but keeping up with these changes is a huge undertaking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safari Browser Fingerprinting
&lt;/h3&gt;

&lt;p&gt;As the second most popular web browser, Safari is slightly more private by default than Chrome. Safari uses &lt;a href="https://webkit.org/blog/7675/intelligent-tracking-prevention/"&gt;Intelligent Tracking Prevention&lt;/a&gt; to determine the sites tracking a user and blocks them if a user hasn't visited them for thirty days. &lt;a href="https://www.apple.com/safari/docs/Safari_White_Paper_Nov_2019.pdf"&gt;Safari now blocks a number of tracking technologies&lt;/a&gt;, including some attempts at fingerprinting, without making any concessions for fraud prevention.&lt;/p&gt;

&lt;p&gt;Like Chrome, Safari lets users disable JavaScript and block cookies. It also shows a privacy report right on the welcome page. Users can disable location services and autofill to prevent those features from being used in fingerprinting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jEYkalwp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lye7546mqlvq0unoglm5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jEYkalwp--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lye7546mqlvq0unoglm5.png" alt="Safari privacy options"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, Safari users can make fingerprinting even harder with extensions like &lt;a href="https://better.fyi/"&gt;Better&lt;/a&gt; or a &lt;a href="https://www.comparitech.com/blog/vpn-privacy/best-vpn-safari/"&gt;VPN&lt;/a&gt; to obscure their IP address, location, and other device-specific data.&lt;/p&gt;

&lt;p&gt;Still, there are some weak spots in Safari’s pro-privacy measures. &lt;a href="https://arstechnica.com/information-technology/2020/03/study-ranks-edges-default-privacy-settings-the-lowest-of-all-major-browsers/"&gt;Researcher Doug Leith&lt;/a&gt; found that the Safari welcome page can actually leak information to third party apps that can then load user identifiers into the browser cache. It’s also possible that Apple’s iCloud processes make connections with identifying user information (likely for debugging purposes). Either of these data points could be part of a browser fingerprint, depending on how the data is distributed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Browser Fingerprinting in Firefox
&lt;/h3&gt;

&lt;p&gt;Firefox has been outspoken about user privacy in recent years. Users are presented with the company’s privacy statement upon opening the browser for the first time, and fingerprint controls are turned on by default.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--TuGSDZUo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1ymdtjb6ovbweqv824g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--TuGSDZUo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q1ymdtjb6ovbweqv824g.png" alt="Firefox standard tracking protection"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This layer of fingerprinting protection built directly into the browser prevents sites from reading:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Your timezone&lt;/li&gt;
&lt;li&gt;Your installed fonts&lt;/li&gt;
&lt;li&gt;Window size preference&lt;/li&gt;
&lt;li&gt;Operating system version&lt;/li&gt;
&lt;li&gt;Keyboard layout and language&lt;/li&gt;
&lt;li&gt;Site-specific zoom settings&lt;/li&gt;
&lt;li&gt;&lt;a href="https://support.mozilla.org/en-US/kb/firefox-protection-against-fingerprinting"&gt;And more&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As in Chrome, Firefox users can change the permissions given to each website they visit, or they can block system resource requests entirely from the &lt;em&gt;Permissions&lt;/em&gt; menu.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--aklViJOS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3imbh7h03pn3jg8s6hca.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--aklViJOS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3imbh7h03pn3jg8s6hca.png" alt="Firefox operating system permissions"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Finally, there are thousands of Firefox extensions that give users more fine-grained &lt;a href="https://addons.mozilla.org/en-US/firefox/search/?q=privacy"&gt;control over their privacy&lt;/a&gt;. Users can also install the &lt;a href="https://addons.mozilla.org/en-US/firefox/addon/amiunique/"&gt;AmIUnique addon&lt;/a&gt; to see how unique their browser is among the millions of fingerprints collected by &lt;a href="https://amiunique.org/"&gt;AmIUnique&lt;/a&gt;. This knowledge can be used by malicious users to tweak their settings further to obscure their identity.&lt;/p&gt;

&lt;p&gt;Even with some pretty strict fingerprinting protections in place, Doug Leith &lt;a href="https://arstechnica.com/information-technology/2020/03/study-ranks-edges-default-privacy-settings-the-lowest-of-all-major-browsers/"&gt;found shortcomings in Firefox’s privacy configuration&lt;/a&gt; too. For example, Firefox transmits identifying information during &lt;a href="https://support.mozilla.org/en-US/kb/telemetry-clientid"&gt;telemetry data reporting&lt;/a&gt; which is on by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ever-Changing World of Browser Fingerprinting
&lt;/h2&gt;

&lt;p&gt;I’ve covered some of the features that browsers offer to users who want to protect their privacy online, but the specifics are constantly changing.&lt;/p&gt;

&lt;p&gt;Browsers need to add new features to enable more complex behavior online, but these new features can often be used to build fingerprints to identify users by their hardware or software. So, the browsers make these features gated or harder to access, but this makes certain websites harder to use.&lt;/p&gt;

&lt;p&gt;Despite the resistance to fingerprinting in some circles, it’s a legitimate and useful tool for preventing fraud and improving online security. With an ever-escalating race between malicious users and fingerprinters, it can be really hard for development teams to keep up with all the changes.&lt;/p&gt;

&lt;p&gt;For example, Firefox &lt;a href="https://www.makeuseof.com/latest-firefox-88-update-elevates-online-privacy/"&gt;just released&lt;/a&gt; an update that prevents sites from reading other open windows' names. If you were maintaining your own fingerprinting software that used open windows as part of your identifying data, you have to decide how to compensate for this change, or your fingerprint will get obsolete quickly.&lt;/p&gt;

&lt;p&gt;This is where tools like &lt;a href="https://fingerprintjs.com/"&gt;FingerprintJS&lt;/a&gt; come in. As experts in the fingerprinting space, they provide developers with 99.5% accurate browser fingerprinting and offer a free, open-source library as well as paid services. &lt;a href="https://fingerprintjs.com/"&gt;FingerprintJS&lt;/a&gt; doesn’t rely on outdated third-party tracking mechanisms, and it helps you prevent account takeovers, password sharing, and fake accounts.&lt;/p&gt;

&lt;p&gt;Modern browsers are doing a good job of improving privacy protections, but this trend comes with a cost to web application owners. Fortunately, fingerprinting is still an accurate and low-cost way to prevent fraud even now. Just don't roll your own.&lt;/p&gt;

</description>
      <category>privacy</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Vulnerability allows cross-browser tracking in Chrome, Firefox, Safari, and Tor</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Thu, 13 May 2021 19:28:40 +0000</pubDate>
      <link>https://forem.com/savannahjs/vulnerability-allows-cross-browser-tracking-in-chrome-firefox-safari-and-tor-39ma</link>
      <guid>https://forem.com/savannahjs/vulnerability-allows-cross-browser-tracking-in-chrome-firefox-safari-and-tor-39ma</guid>
      <description>&lt;p&gt;&lt;em&gt;In this article we introduce a scheme flooding vulnerability, explain how the exploit works across four major desktop browsers and show why it's a threat to anonymous browsing.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdwtcjvswhog1dql4fxoi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdwtcjvswhog1dql4fxoi.png" title="Scheme flood vulnerability overview" alt="Exploiting custom protocol handlers for cross-browser tracking in Tor, Safari, Chrome and Firefox"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DISCLAIMER:&lt;/strong&gt; FingerprintJS does not use this vulnerability in our products and does not provide third-party tracking services. We focus on stopping fraud and support modern privacy trends for removing third-party tracking entirely. We believe that vulnerabilities like this one should be discussed in the open to help browsers fix them as quickly as possible. To help fix it, we have submitted bug reports to all affected browsers, created a live demo and have made a &lt;a href="https://github.com/fingerprintjs/external-protocol-flooding" rel="noopener noreferrer"&gt;public source code repository&lt;/a&gt; available to all.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Test the vulnerability on our &lt;a href="https://schemeflood.com/" rel="noopener noreferrer"&gt;live demo site&lt;/a&gt;. Works on desktop browsers only.&lt;/strong&gt;
&lt;/h3&gt;



&lt;p&gt;In our research into anti-fraud techniques, we have discovered a vulnerability that allows websites to identify users reliably across different desktop browsers and link their identities together. The desktop versions of Tor Browser, Safari, Chrome, and Firefox are all affected.&lt;/p&gt;

&lt;p&gt;We will be referring to this vulnerability as scheme flooding, as it uses custom URL schemes as an attack vector. The vulnerability uses information about installed apps on your computer in order to assign you a permanent unique identifier even if you switch browsers, use incognito mode, or use a VPN.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why does this matter? 
&lt;/h2&gt;

&lt;p&gt;The scheme flooding vulnerability allows for third party tracking across different browsers and thus is a violation of privacy.&lt;/p&gt;

&lt;h3&gt;
  
  
  No cross-browser anonymity
&lt;/h3&gt;

&lt;p&gt;Cross-browser anonymity is something that even a privacy conscious internet user may take for granted. Tor Browser is known to offer the ultimate in privacy protection, though due to its slow connection speed and performance issues on some websites, users may rely on less anonymous browsers for their every day surfing. They may use Safari, Firefox or Chrome for some sites, and Tor for sites where they want to stay anonymous. A website exploiting the scheme flooding vulnerability could create a stable and unique identifier that can link those browsing identities together.&lt;/p&gt;

&lt;p&gt;Even if you are not a Tor Browser user, all major browsers are affected. It’s possible to link your Safari visit to your Chrome visit, identify you uniquely and track you across the web.&lt;/p&gt;

&lt;h3&gt;
  
  
  Profiling based on installed apps
&lt;/h3&gt;

&lt;p&gt;Additionally, the scheme flood vulnerability allows for targeted advertisement and user profiling without user consent. The list of installed applications on your device can reveal a lot about your occupation, habits, and age. For example, if a Python IDE or a PostgreSQL server is installed on your computer, you are very likely to be a backend developer. &lt;/p&gt;

&lt;p&gt;Depending on the apps installed on a device, it may be possible for a website to identify individuals for more sinister purposes. For example, a site may be able to detect a government or military official on the internet based on their installed apps and associate browsing history that is intended to be anonymous.&lt;/p&gt;

&lt;h3&gt;
  
  
  Unknown impact on the web
&lt;/h3&gt;

&lt;p&gt;This vulnerability has been possible for more than 5 years and its true impact is unknown. In a quick search of the web, we couldn’t find any website actively exploiting it but we still felt the need to report it as soon as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work? (technical overview)
&lt;/h2&gt;

&lt;p&gt;Note: You may skip this section if you are not interested in the technical implementation details. The source code of the &lt;a href="https://schemeflood.com/" rel="noopener noreferrer"&gt;demo&lt;/a&gt; application is available on &lt;a href="https://github.com/fingerprintjs/external-protocol-flooding" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The scheme flooding vulnerability allows an attacker to determine which applications you have installed. In order to generate a 32-bit cross-browser device identifier, a website can test a list of 32 popular applications and check if each is installed or not. On average, the identification process takes a few seconds and works across desktop Windows, Mac and Linux operating systems. &lt;/p&gt;

&lt;p&gt;To check if an application is installed, browsers can use built-in custom URL scheme handlers. You can see this feature in action by entering skype:// in your browser address bar. If you have Skype installed, your browser will open a confirmation dialog that asks if you want to launch it. This feature is also known as deep linking and is widely used on mobile devices, but is available within desktop browsers as well. Any application that you install can register its own scheme to allow other apps to open it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F71z0lr0okuvq7avl74ee.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F71z0lr0okuvq7avl74ee.png" title="Scheme handler" alt="An example of a custom URL scheme handler for Zoom"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In order to detect if an application is installed, we can test an application's custom URL scheme and then check if a popup has been shown. &lt;/p&gt;

&lt;p&gt;To make this vulnerability possible, the following steps are required:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Prepare a list of application URL schemes that you want to test. The list may depend on your goals, for example, if you want to check if some industry or interest-specific applications are installed.&lt;/li&gt;
&lt;li&gt;Add a script on a website that will test each application from your list. The script will return an ordered array of boolean values. Each boolean value is true if the application is installed or false if it is not.&lt;/li&gt;
&lt;li&gt;Use this array to generate a permanent cross-browser identifier. &lt;/li&gt;
&lt;li&gt;Optionally, use machine learning algorithms to guess your website visitors’ occupation, interests, and age using installed application data.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The steps above may sound easy, but most browsers have safety mechanisms in place designed to prevent such exploits. Weaknesses in these safety mechanisms are what makes this vulnerability possible. A combination of &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS" rel="noopener noreferrer"&gt;CORS policies&lt;/a&gt; and browser window features can be used to bypass it.&lt;/p&gt;

&lt;p&gt;The actual implementation of the exploit varies by browser, however the basic concept is the same. It works by asking the browser to show a confirmation dialog in a popup window. Then the JavaScript code can detect if a popup has just been opened and detect the presence of an application based on that.&lt;/p&gt;

&lt;p&gt;Let’s walk through some of the browser differences.&lt;/p&gt;

&lt;h3&gt;
  
  
  Chrome
&lt;/h3&gt;

&lt;p&gt;Of the four major browsers impacted, only Chrome developers appear to be aware of the scheme flooding vulnerability. The issue has been discussed on the &lt;a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1096610" rel="noopener noreferrer"&gt;Chromium bug-tracker&lt;/a&gt; and is planned to be fixed soon.&lt;/p&gt;

&lt;p&gt;Additionally, only the Chrome browser had any form of scheme flood protection which presented a challenge to bypass. It prevents launching any application unless requested by a user gesture, like a mouse click. There is a global flag that allows (or denies) websites to open applications, which is set to false after handling a custom URL scheme.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj62wlimstn1mutp7t8ji.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj62wlimstn1mutp7t8ji.png" title="Scheme flood protection in Chrome" alt="An example of how the Chrome browser protects against scheme flooding by requiring a user gesture"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, you can use Chrome extensions to reset this flag and bypass the scheme flood protection. By &lt;a href="https://chromium.googlesource.com/chromium/src/+/refs/heads/main/chrome/browser/external_protocol/external_protocol_handler.h#125" rel="noopener noreferrer"&gt;specification&lt;/a&gt;, extensions need to be able to open custom URLs, such as mailto: links, without confirmation dialogs. The scheme flood protection conflicts with extension policies so there is a loophole that resets this flag every time any extension is triggered.&lt;/p&gt;

&lt;p&gt;The built-in Chrome PDF Viewer is an extension, so every time your browser opens a PDF file it resets the scheme flood protection flag. Opening a PDF file before opening a custom URL makes the exploit functional.&lt;/p&gt;

&lt;h3&gt;
  
  
  Firefox
&lt;/h3&gt;

&lt;p&gt;Every time you navigate to an unknown URL scheme, Firefox will show you an internal page with an error. This internal page has a different origin than any other website, so it is impossible to access it because of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Security/Same-origin_policy" rel="noopener noreferrer"&gt;Same-origin policy&lt;/a&gt; limitation. On the other hand, a known custom URL scheme will be opened as about:blank, whose origin will be accessible from the current website.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvcp793rhx0oi1t8vvsyr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvcp793rhx0oi1t8vvsyr.png" title="URL scheme examples" alt="An example of how you can check if an application is installed based on the origin URL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;By opening a popup window with a custom URL scheme and checking if its document is available from JavaScript code, you can detect if the application is installed on the device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Safari
&lt;/h3&gt;

&lt;p&gt;Despite privacy being a main development focus for the Safari browser, it turned out to be the easiest browser of the four to exploit. Safari doesn’t have scheme flood protection and allows to easily enumerate all installed applications.&lt;/p&gt;

&lt;p&gt;The same-origin policy trick as used for the Firefox browser was used here as well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tor Browser
&lt;/h3&gt;

&lt;p&gt;Tor Browser has confirmation dialogs disabled entirely as a privacy feature, which, ironically, exposed a more damaging vulnerability for this particular exploit. Nothing is shown while the exploit runs in the background, contrasting with other browsers that show pop-ups during the process. This oversight allows the exploit to check through installed applications without users even realizing it.&lt;/p&gt;

&lt;p&gt;Tor Browser is based on the Firefox source code, so the Same-origin policy trick was used here as well. But because Tor Browser does not show pop-ups, we used the same-origin policy trick with iframe elements instead.&lt;/p&gt;

&lt;p&gt;By creating an iframe element with a custom URL scheme and checking if its document is available you can check if the application is installed or not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8iex3eoikf53jf2yz4y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv8iex3eoikf53jf2yz4y.png" title="iframe element with custom URL scheme" alt="An example of how you can use an iframe element to check if an application is installed"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of the four browsers, the scheme flooding vulnerability takes the longest to successfully run in Tor.  It can take up to 10 seconds for each application to be checked due to Tor Browser policies. Still, the exploit can be made to work in the background and track you over a longer browsing session. If you left a Tor Browser window on a web page only for 4 minutes, it could be enough to expose your identity.&lt;/p&gt;

&lt;p&gt;It’s possible to remove the 10-second limitation by running each application test inside a user-triggered gesture. A fake captcha is an ideal candidate: 24 characters entered by a user makes it possible to reset that 10-second limitation 24 times in a row and enumerate 24 installed applications instantaneously.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The exact steps to make the scheme flooding vulnerability possible may vary by browser, but the end result is the same. Getting a unique array of bits associated with a visitor’s identity is not only possible, but can be used on malicious websites in practice. Even Tor Browser can be effectively exploited by tricking a user into typing one character per application we want to test.&lt;/p&gt;

&lt;p&gt;Until this vulnerability is fixed, the only way to have private browsing sessions not associated with your primary device is to use another device altogether. &lt;/p&gt;

&lt;p&gt;By submitting these bug reports, writing this article and building our demo application, we hope that this vulnerability is fixed across all browsers as soon as possible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://schemeflood.com/" rel="noopener noreferrer"&gt;Demo&lt;/a&gt; (works only on desktop)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fingerprintjs/external-protocol-flooding" rel="noopener noreferrer"&gt;Repository with all sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bugs.webkit.org/show_bug.cgi?id=225769" rel="noopener noreferrer"&gt;Bug report for Safari&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bugs.chromium.org/p/chromium/issues/detail?id=1096610" rel="noopener noreferrer"&gt;Bug report for Chrome&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1711084" rel="noopener noreferrer"&gt;Bug report for Firefox&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you enjoyed reading this article, consider joining our fully remote team to work on exciting research in online security: &lt;a href="//mailto:work@fingerprintjs.com"&gt;work@fingerprintjs.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>privacy</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How the Web Audio API is used for browser fingerprinting</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Tue, 23 Mar 2021 01:24:00 +0000</pubDate>
      <link>https://forem.com/savannahjs/how-the-web-audio-api-is-used-for-browser-fingerprinting-4oim</link>
      <guid>https://forem.com/savannahjs/how-the-web-audio-api-is-used-for-browser-fingerprinting-4oim</guid>
      <description>&lt;p&gt;Did you know that you can identify web browsers without using cookies or asking for permissions?&lt;/p&gt;

&lt;p&gt;This is known as “browser fingerprinting” and it works by reading browser attributes and combining them together into a single identifier. This identifier is stateless and works well in normal and incognito modes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzhdfywqt5tg2qxl5y82.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzhdfywqt5tg2qxl5y82.png" alt="Graphic of browser fingerprinting"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When generating a browser identifier, we can read browser attributes directly or use attribute processing techniques first. One of the creative techniques that we’ll discuss today is audio fingerprinting.&lt;/p&gt;

&lt;p&gt;Audio fingerprinting is a valuable technique because it is relatively unique and stable. Its uniqueness comes from the internal complexity and sophistication of the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Audio_API" rel="noopener noreferrer"&gt;&lt;span&gt;Web Audio API&lt;/span&gt;&lt;/a&gt;. The stability is achieved because the audio source that we’ll use is a sequence of numbers, generated mathematically. Those numbers will later be combined into a single audio fingerprint value.&lt;/p&gt;

&lt;p&gt;Before we dive into the technical implementation, we need to understand a few ideas from the Web Audio API and its building blocks.&lt;/p&gt;

&lt;h1&gt;
  
  
  A brief overview of the Web Audio API
&lt;/h1&gt;

&lt;p&gt;The Web Audio API is a powerful system for handling audio operations. It is designed to work inside an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioContext" rel="noopener noreferrer"&gt;&lt;code&gt;AudioContext&lt;/code&gt;&lt;/a&gt; by linking together audio nodes and building an audio graph. A single &lt;code&gt;AudioContext&lt;/code&gt; can handle multiple types of audio sources that plug into other nodes and form chains of audio processing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwf1wz9mwqjltpr2gqj70.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fwf1wz9mwqjltpr2gqj70.png" alt="Graphic of audio context"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A source can be an &lt;code&gt;audio&lt;/code&gt; element, a stream, or an in-memory source generated mathematically with an &lt;code&gt;Oscillator&lt;/code&gt;. We’ll be using the &lt;code&gt;Oscillator&lt;/code&gt; for our purposes and then connecting it to other nodes for additional processing.&lt;/p&gt;

&lt;p&gt;Before we dive into the audio fingerprint implementation details, it’s helpful to review all of the building blocks of the API that we’ll be using.&lt;/p&gt;

&lt;h2&gt;
  
  
  AudioContext
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;AudioContext&lt;/code&gt; represents an entire chain built from audio nodes linked together. It controls the creation of the nodes and execution of the audio processing. You always start by creating an instance of &lt;code&gt;AudioContext&lt;/code&gt; before you do anything else. It’s a good practice to create a single &lt;code&gt;AudioContext&lt;/code&gt; instance and reuse it for all future processing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AudioContext&lt;/code&gt; has a destination property that represents the destination of all audio from that context. &lt;/p&gt;

&lt;p&gt;There also exists a special type of &lt;code&gt;AudioContext&lt;/code&gt;: &lt;code&gt;OfflineAudioContext&lt;/code&gt;. The main difference is that it does not render the audio to the device hardware. Instead, it generates the audio as fast as possible and saves it into an &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/AudioBuffer" rel="noopener noreferrer"&gt;&lt;code&gt;AudioBuffer&lt;/code&gt;&lt;/a&gt;. Thus, the destination of the OfflineAudioContext will be an in-memory data structure, while with a regular AudioContext, the destination will be an audio-rendering device.&lt;/p&gt;

&lt;p&gt;When creating an instance of &lt;code&gt;OfflineAudioContext&lt;/code&gt;, we pass &lt;code&gt;3&lt;/code&gt; arguments: the number of channels, the total number of samples and a sample rate in samples per second.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AudioContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; 
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OfflineAudioContext&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitOfflineAudioContext&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  AudioBuffer
&lt;/h2&gt;

&lt;p&gt;An &lt;code&gt;AudioBuffer&lt;/code&gt; represents an audio snippet, stored in memory. It’s designed to hold small snippets. The data is represented internally in Linear PCM with each sample represented by a &lt;code&gt;32&lt;/code&gt;-bit float between &lt;code&gt;-1.0&lt;/code&gt; and &lt;code&gt;1.0.&lt;/code&gt; It can hold multiple channels, but for our purposes we’ll use only one channel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5mr8g7r1wkei5rh3n16j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5mr8g7r1wkei5rh3n16j.png" alt="Diagram of 32-bit numbers"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Oscillator
&lt;/h2&gt;

&lt;p&gt;When working with audio, we always need a source. An &lt;code&gt;oscillator&lt;/code&gt; is a good candidate, because it generates samples mathematically, as opposed to playing an audio file. In its simplest form, an &lt;code&gt;oscillator&lt;/code&gt; generates a periodic waveform with a specified frequency. &lt;/p&gt;

&lt;p&gt;The default shape is a sine wave.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1et2v2sezvk9i929q3c.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh1et2v2sezvk9i929q3c.PNG" alt="Sine wave"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;We made a live demo of this! You can play with &lt;a href="https://fingerprintjs.com/blog/audio-fingerprinting/" rel="noopener noreferrer"&gt;the real deal&lt;/a&gt; on our blog.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It’s also possible to generate other types of waves, such as square, sawtooth, and triangle.&lt;/p&gt;

&lt;p&gt;The default frequency is &lt;code&gt;440&lt;/code&gt; Hz, which is a standard A4 note.&lt;/p&gt;
&lt;h2&gt;
  
  
  Compressor
&lt;/h2&gt;

&lt;p&gt;The Web Audio API provides a &lt;code&gt;DynamicsCompressorNode&lt;/code&gt;, which lowers the volume of the loudest parts of the signal and helps prevent distortion or clipping. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;DynamicsCompressorNode&lt;/code&gt; has many interesting properties that we’ll use. These properties will help create more variability between browsers.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Threshold&lt;/code&gt; - value in decibels above which the compressor will start taking effect.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Knee&lt;/code&gt; - value in decibels representing the range above the threshold where the curve smoothly transitions to the compressed portion.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ratio&lt;/code&gt; - amount of input change, in dB, needed for a &lt;code&gt;1&lt;/code&gt; dB change in the output.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Reduction&lt;/code&gt; - float representing the amount of gain reduction currently applied by the compressor to the signal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Attack&lt;/code&gt; - the amount of time, in seconds, required to reduce the gain by &lt;code&gt;10&lt;/code&gt; dB. This value can be a decimal.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Release&lt;/code&gt; - the amount of time, in seconds, required to increase the gain by &lt;code&gt;10&lt;/code&gt; dB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopjg23igllvquo87pogv.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fopjg23igllvquo87pogv.PNG" alt="Compressor attributes"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;We made a live demo of this! You can play with &lt;a href="https://fingerprintjs.com/blog/audio-fingerprinting/" rel="noopener noreferrer"&gt;the real deal&lt;/a&gt; on our blog.&lt;/em&gt;&lt;/p&gt;

 

&lt;h2&gt;
  
  
  How the audio fingerprint is calculated
&lt;/h2&gt;

&lt;p&gt;Now that we have all the concepts we need, we can start working on our audio fingerprinting code.&lt;/p&gt;

&lt;p&gt;Safari doesn’t support unprefixed &lt;code&gt;OfflineAudioContext&lt;/code&gt;, but does support &lt;br&gt;
&lt;code&gt;webkitOfflineAudioContext&lt;/code&gt;, so we’ll use this method to make it work in Chrome and Safari:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;AudioContext&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OfflineAudioContext&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webkitOfflineAudioContex&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we create an &lt;code&gt;AudioContext&lt;/code&gt; instance. We’ll use one channel, a &lt;code&gt;44,100&lt;/code&gt; sample rate and &lt;code&gt;5,000&lt;/code&gt; samples total, which will make it about &lt;code&gt;113&lt;/code&gt; ms long.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next let’s create a sound source - an &lt;code&gt;oscillator&lt;/code&gt; instance. It will generate a triangular-shaped sound wave that will fluctuate &lt;code&gt;1,000&lt;/code&gt; times per second (&lt;code&gt;1,000 Hz&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oscillator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createOscillator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;triangle&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;frequency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now let’s create a compressor to add more variety and transform the original signal. Note that the values for all these parameters are arbitrary and are only meant to change the source signal in interesting ways. We could use other values and it would still work.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compressor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createDynamicsCompressor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;
&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;knee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;
&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reduction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;release&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let’s connect our nodes together: &lt;code&gt;oscillator&lt;/code&gt; to &lt;code&gt;compressor&lt;/code&gt;, and compressor to the context destination.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;compressor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is time to generate the audio snippet. We’ll use the &lt;code&gt;oncomplete&lt;/code&gt; event to get the result when it’s ready.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;oscillator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oncomplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// We have only one channel, so we get it by index&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderedBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startRendering&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Samples&lt;/code&gt; is an array of floating-point values that represents the uncompressed sound. Now we need to calculate a single value from that array.&lt;/p&gt;

&lt;p&gt;Let’s do it by simply summing up a slice of the array values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;calculateHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;hash&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;getHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we are ready to generate the audio fingerprint. When I run it on Chrome on MacOS I get the value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;101.45647543197447&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That’s all there is to it. Our audio fingerprint is this number!&lt;/p&gt;

&lt;p&gt;You can check out a &lt;a href="https://github.com/fingerprintjs/fingerprintjs/blob/3201a7d61bb4df2816c226d8364cc98bb4235e59/src/sources/audio.ts" rel="noopener noreferrer"&gt;&lt;span&gt;production implementation&lt;/span&gt;&lt;/a&gt; in our open source browser fingerprinting library.&lt;/p&gt;

&lt;p&gt;If I try executing the code in Safari, I get a different number:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;79.58850509487092&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And get another unique result in Firefox:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;80.95458510611206&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every browser we have on our testing laptops generate a different value. This value is very stable and remains the same in incognito mode.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This value depends on the underlying hardware and OS, and in your case may be different.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the audio fingerprint varies by browser
&lt;/h2&gt;

&lt;p&gt;Let’s take a closer look at why the values are different in different browsers. We’ll examine a single oscillation wave in both Chrome and Firefox.&lt;/p&gt;

&lt;p&gt;First, let’s reduce the duration of our audio snippet to &lt;code&gt;1/2000th&lt;/code&gt; of a second, which corresponds to a single wave and examine the values that make up that wave.&lt;/p&gt;

&lt;p&gt;We need to change our context duration to &lt;code&gt;23&lt;/code&gt; samples, which roughly corresponds to a &lt;code&gt;1/2000th&lt;/code&gt; of a second. We’ll also skip the compressor for now and only examine the differences of the unmodified &lt;code&gt;oscillator&lt;/code&gt; signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;23&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here is how a single triangular oscillation looks in both Chrome and Firefox now:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfdsr77088c30tgjqazv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdfdsr77088c30tgjqazv.png" alt="One wave"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However the underlying values are different between the two browsers (I’m showing only the first &lt;code&gt;3&lt;/code&gt; values for simplicity):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;Chrome:&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;&lt;code&gt;Firefox:&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.08988945186138153&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.09155717492103577&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.18264609575271606&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.18603470921516418&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0.2712443470954895&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.2762767672538757&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let’s take a look at this demo to visually see those differences.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p02qwo2vxw3bdo9umvr.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4p02qwo2vxw3bdo9umvr.PNG" alt="Audio wave by browser"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;We made a live demo of this! You can play with &lt;a href="https://fingerprintjs.com/blog/audio-fingerprinting/" rel="noopener noreferrer"&gt;the real deal&lt;/a&gt; on our blog.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Historically, all major browser engines (Blink, WebKit, and Gecko) based their Web Audio API implementations on code that was originally developed by Google in &lt;code&gt;2011&lt;/code&gt; and &lt;code&gt;2012&lt;/code&gt; for the WebKit project.&lt;/p&gt;

&lt;p&gt;Examples of Google contributions to the Webkit project include:&lt;br&gt;
&lt;a href="https://github.com/WebKit/WebKit/commit/d187ecab7b152962465c23be04ab7ed3ef70f382" rel="noopener noreferrer"&gt;&lt;span&gt;creation of &lt;code&gt;OfflineAudioContext&lt;/code&gt;&lt;/span&gt;&lt;/a&gt;, &lt;br&gt;
&lt;a href="https://github.com/WebKit/WebKit/commit/fad97bfb064446f78c78338104fb3f22be666cbb" rel="noopener noreferrer"&gt;&lt;span&gt;creation of &lt;code&gt;OscillatorNode&lt;/code&gt;&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://github.com/WebKit/WebKit/commit/6f2b47e87bc414001affb258048749130bc91083" rel="noopener noreferrer"&gt;&lt;span&gt;creation of DynamicsCompressorNode&lt;/span&gt;&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Since then browser developers have made a lot of small changes. These changes, compounded by the large number of mathematical operations involved, lead to fingerprinting differences. Audio signal processing uses floating point arithmetic, which also contributes to discrepancies in calculations.&lt;/p&gt;

&lt;p&gt;You can see how these things are implemented now in the three major browser engines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Blink: &lt;a href="https://github.com/chromium/chromium/blob/9841ee86b710dc649cf41772f560600324cadf45/third_party/blink/renderer/modules/webaudio/periodic_wave.cc#L468" rel="noopener noreferrer"&gt;&lt;span&gt;oscillator&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://github.com/chromium/chromium/blob/3e914531a360b766bfd8468f59259b3ab29118d7/third_party/blink/renderer/platform/audio/dynamics_compressor_kernel.cc#L202" rel="noopener noreferrer"&gt;&lt;span&gt;dynamics compressor&lt;/span&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;WebKit: &lt;a href="https://github.com/WebKit/WebKit/blob/010d252ab89d2c867efcba547e879c11968eebe7/Source/WebCore/Modules/webaudio/PeriodicWave.cpp#L250" rel="noopener noreferrer"&gt;&lt;span&gt;oscillator&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://github.com/WebKit/WebKit/blob/010d252ab89d2c867efcba547e879c11968eebe7/Source/WebCore/platform/audio/DynamicsCompressorKernel.cpp#L188" rel="noopener noreferrer"&gt;&lt;span&gt;dynamics compressor&lt;/span&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Gecko: &lt;a href="https://github.com/mozilla/gecko-dev/blob/9ae77e4ce3378bd683ac9a86b729ea6b6bd22cb8/dom/media/webaudio/blink/PeriodicWave.cpp#L286" rel="noopener noreferrer"&gt;&lt;span&gt;oscillator&lt;/span&gt;&lt;/a&gt;, &lt;a href="https://github.com/mozilla/gecko-dev/blob/9ae77e4ce3378bd683ac9a86b729ea6b6bd22cb8/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp#L213" rel="noopener noreferrer"&gt;&lt;span&gt;dynamics compressor&lt;/span&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Additionally, browsers use different implementations for different CPU architectures and OSes to leverage features like &lt;a href="https://en.wikipedia.org/wiki/SIMD" rel="noopener noreferrer"&gt;&lt;span&gt;SIMD&lt;/span&gt;&lt;/a&gt;. For example, Chrome uses &lt;a href="https://github.com/chromium/chromium/blob/3e914531a360b766bfd8468f59259b3ab29118d7/third_party/blink/renderer/platform/audio/mac/fft_frame_mac.cc" rel="noopener noreferrer"&gt;&lt;span&gt;a separate fast Fourier transform implementation&lt;/span&gt;&lt;/a&gt; on macOS (producing a different &lt;code&gt;oscillator&lt;/code&gt; signal) and &lt;a href="https://github.com/chromium/chromium/tree/3e914531a360b766bfd8468f59259b3ab29118d7/third_party/blink/renderer/platform/audio/cpu" rel="noopener noreferrer"&gt;&lt;span&gt;different vector operation implementations&lt;/span&gt;&lt;/a&gt; on different CPU architectures (which are used in the DynamicsCompressor implementation). These platform-specific changes also contribute to differences in the final audio fingerprint.&lt;/p&gt;

&lt;p&gt;Fingerprint results also depend on the Android version (it’s different in Android &lt;code&gt;9&lt;/code&gt; and &lt;code&gt;10&lt;/code&gt; on the same devices for example).&lt;/p&gt;

&lt;p&gt;According to browser source code, audio processing doesn’t use dedicated audio hardware or OS features—all calculations are done by the CPU. &lt;/p&gt;
&lt;h2&gt;
  
  
  Pitfalls
&lt;/h2&gt;

&lt;p&gt;When we started to use audio fingerprinting in production, we aimed to achieve good browser compatibility, stability and performance. For high browser compatibility, we also looked at privacy-focused browsers, such as Tor and Brave.&lt;/p&gt;
&lt;h3&gt;
  
  
  OfflineAudioContext
&lt;/h3&gt;

&lt;p&gt;As you can see on &lt;a href="https://caniuse.com/mdn-api_offlineaudiocontext" rel="noopener noreferrer"&gt;&lt;span&gt;caniuse.com&lt;/span&gt;&lt;/a&gt;, &lt;code&gt;OfflineAudioContext&lt;/code&gt; works almost everywhere. But there are some cases that need special handling.&lt;/p&gt;

&lt;p&gt;The first case is iOS &lt;code&gt;11&lt;/code&gt; or older. It does support &lt;code&gt;OfflineAudioContext&lt;/code&gt;, but the rendering only starts if &lt;a href="https://stackoverflow.com/a/46534088/1118709" rel="noopener noreferrer"&gt;&lt;span&gt;triggered by a user action&lt;/span&gt;&lt;/a&gt;, for example by a button click. If &lt;code&gt;context.startRendering&lt;/code&gt; is not triggered by a user action, the &lt;code&gt;context.state&lt;/code&gt; will be &lt;code&gt;suspended&lt;/code&gt; and rendering will hang indefinitely unless you add a timeout. There are not many users who still use this iOS version, so we decided to disable audio fingerprinting for them.&lt;/p&gt;

&lt;p&gt;The second case are browsers on iOS &lt;code&gt;12&lt;/code&gt; or newer. They can reject starting audio processing if the page is in the background. Luckily, browsers allow you to resume the processing when the page returns to the foreground.&lt;br&gt;
When the page is activated, we attempt calling &lt;code&gt;context.startRendering()&lt;/code&gt; several times until the &lt;code&gt;context.state&lt;/code&gt; becomes &lt;code&gt;running&lt;/code&gt;. If the processing doesn’t start after several attempts, the code stops. We also use a regular &lt;code&gt;setTimeout&lt;/code&gt; on top of our retry strategy in case of an unexpected error or freeze. You can see &lt;a href="https://gist.github.com/Finesse/92959ce907a5ba7ee5c05542e3f8741b" rel="noopener noreferrer"&gt;&lt;span&gt;a code example here&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;
  
  
  Tor
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;In the case of the Tor browser, everything is simple. Web Audio API is disabled there, so audio fingerprinting is &lt;a href="https://gitlab.torproject.org/legacy/trac/-/issues/21984" rel="noopener noreferrer"&gt;&lt;span&gt;not possible&lt;/span&gt;&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Brave
&lt;/h3&gt;

&lt;p&gt;With Brave, the situation is more nuanced. Brave is a privacy-focused browser based on Blink. It is known to slightly randomize the audio sample values, which it calls “farbling”.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Farbling is Brave’s term for slightly randomizing the output of semi-identifying browser features, in a way that’s difficult for websites to detect, but doesn’t break benign, user-serving websites. These “farbled” values are deterministically generated using a per-session, &lt;a href="https://publicsuffix.org/" rel="noopener noreferrer"&gt;&lt;span&gt;per-eTLD&lt;/span&gt;&lt;/a&gt;+1 seed so that a site will get the exact same value each time it tries to fingerprint within the same session, but that different sites will get different values, and the same site will get different values on the next session. This technique has its roots in prior privacy research, including the &lt;a href="https://dl.acm.org/doi/abs/10.1145/2736277.2741090" rel="noopener noreferrer"&gt;&lt;span&gt;PriVaricator&lt;/span&gt;&lt;/a&gt; (Nikiforakis et al, WWW 2015) and &lt;a href="https://hal.inria.fr/hal-01527580/document" rel="noopener noreferrer"&gt;&lt;span&gt;FPRandom&lt;/span&gt;&lt;/a&gt; (Laperdrix et al, ESSoS 2017) projects.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Brave offers three levels of farbling (users can choose the level they want in settings):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disabled — no farbling is applied. The fingerprint is the same as in other Blink browsers such as Chrome.&lt;/li&gt;
&lt;li&gt;Standard — This is the default value. The audio signal values are multiplied by a fixed number, called the “fudge” factor, that is stable for a given domain within a user session. In practice it means that the audio wave sounds and looks the same, but has tiny variations that make it difficult to use in fingerprinting.&lt;/li&gt;
&lt;li&gt;Strict — the sound wave is replaced with a pseudo-random sequence.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The farbling &lt;a href="https://github.com/brave/brave-core/blob/680b0d872e0a295ef94602fb5dc1907358d6a3ba/chromium_src/third_party/blink/renderer/modules/webaudio/audio_buffer.cc#L16" rel="noopener noreferrer"&gt;&lt;span&gt;modifies&lt;/span&gt;&lt;/a&gt; the original Blink &lt;code&gt;AudioBuffer&lt;/code&gt; by &lt;a href="https://github.com/brave/brave-core/blob/680b0d872e0a295ef94602fb5dc1907358d6a3ba/chromium_src/third_party/blink/renderer/core/execution_context/execution_context.cc#L133" rel="noopener noreferrer"&gt;&lt;span&gt;transforming&lt;/span&gt;&lt;/a&gt; the original audio values.&lt;/p&gt;
&lt;h3&gt;
  
  
  Reverting Brave standard farbling
&lt;/h3&gt;

&lt;p&gt;To revert the farbling, we need to obtain the fudge factor first. Then we can get back the original buffer by dividing the farbled values by the fudge factor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getFudgeFactor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AudioContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;44100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;inputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;inputNode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createBufferSource&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;inputNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;inputBuffer&lt;/span&gt;
  &lt;span class="nx"&gt;inputNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;inputNode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

  &lt;span class="c1"&gt;// See the renderAudio implementation &lt;/span&gt;
  &lt;span class="c1"&gt;// at https://git.io/Jmw1j&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;outputBuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;renderAudio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;outputBuffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChannelData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;fingerprint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;fudgeFactor&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
  &lt;span class="c1"&gt;// This function is the fingerprint algorithm described&lt;/span&gt;
  &lt;span class="c1"&gt;// in the “How audio fingerprint is calculated” section&lt;/span&gt;
  &lt;span class="nf"&gt;getFingerprint&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="nf"&gt;getFudgeFactor&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;restoredFingerprint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fingerprint&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;fudgeFactor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Unfortunately, floating point operations lack the required precision to get the original samples exactly. The table below shows restored audio fingerprint in different cases and shows how close they are to the original values:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;OS, browser&lt;/th&gt;
&lt;th&gt;Fingerprint&lt;/th&gt;
&lt;th&gt;Absolute difference between the target fingerprint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;macOS 11, Chrome 89 (the target fingerprint)&lt;/td&gt;
&lt;td&gt;124.0434806260746&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS 11, Brave 1.21 (same device and OS)&lt;/td&gt;
&lt;td&gt;Various fingerprints after browser restarts:&lt;br&gt;124.04347912294482&lt;br&gt;124.0434832855703&lt;br&gt;124.04347889351203&lt;br&gt;124.04348024313667&lt;/td&gt;
&lt;td&gt;0.00000014% – 0.00000214%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows 10, Chrome 89&lt;/td&gt;
&lt;td&gt;124.04347527516074&lt;/td&gt;
&lt;td&gt;0.00000431%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows 10, Brave 1.21&lt;/td&gt;
&lt;td&gt;Various fingerprints after browser restarts:&lt;br&gt;124.04347610535537&lt;br&gt;124.04347187270707&lt;br&gt;124.04347220244154&lt;br&gt;124.04347384813703&lt;/td&gt;
&lt;td&gt;0.00000364% – 0.00000679%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android 11, Chrome 89&lt;/td&gt;
&lt;td&gt;124.08075528279005&lt;/td&gt;
&lt;td&gt;0.03%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Android 9, Chrome 89&lt;/td&gt;
&lt;td&gt;124.08074500028306&lt;/td&gt;
&lt;td&gt;0.03%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChromeOS 89&lt;/td&gt;
&lt;td&gt;124.04347721464&lt;/td&gt;
&lt;td&gt;0.00000275%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS 11, Safari 14&lt;/td&gt;
&lt;td&gt;35.10893232002854&lt;/td&gt;
&lt;td&gt;71.7%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS 11, Firefox 86&lt;/td&gt;
&lt;td&gt;35.7383295930922&lt;/td&gt;
&lt;td&gt;71.2%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As you can see, the restored Brave fingerprints are closer to the original fingerprints than to other browsers’ fingerprints. This means that you can use a fuzzy algorithm to match them. For example, if the difference between a pair of audio fingerprint numbers is more than &lt;code&gt;0.0000022%&lt;/code&gt;, you can assume that these are different devices or browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Performance
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Web Audio API rendering
&lt;/h3&gt;

&lt;p&gt;Let’s take a look at what happens under the hood in Chrome during audio fingerprint generation. In the screenshot below, the horizontal axis is time, the rows are execution threads, and the bars are time slices when the browser is busy. You can learn more about the performance panel in this &lt;a href="https://developers.google.com/web/tools/chrome-devtools/evaluate-performance" rel="noopener noreferrer"&gt;&lt;span&gt;Chrome article&lt;/span&gt;&lt;/a&gt;. The audio processing starts at &lt;code&gt;809.6 ms&lt;/code&gt; and completes at &lt;code&gt;814.1 ms&lt;/code&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="/img/uploads/performance.jpg" class="article-body-image-wrapper"&gt;&lt;img src="/img/uploads/performance.jpg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main thread, labeled as “Main” on the image, handles user input (mouse movements, clicks, taps, etc) and animation. When the main thread is busy, the page freezes. It’s a good practice to avoid running blocking operations on the main thread for more than several milliseconds. &lt;/p&gt;

&lt;p&gt;As you can see on the image above, the browser delegates some work to the &lt;code&gt;OfflineAudioRender&lt;/code&gt; thread, freeing the main thread. &lt;br&gt;
&lt;strong&gt;Therefore the page stays responsive during most of the audio fingerprint calculation.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt; Web Audio API is not available in &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API" rel="noopener noreferrer"&gt;&lt;span&gt;web workers&lt;/span&gt;&lt;/a&gt;, so we cannot calculate audio fingerprints there.&lt;/p&gt;

&lt;h3&gt;
  
  
  Performance summary in different browsers
&lt;/h3&gt;

&lt;p&gt;The table below shows the time it takes to get a fingerprint on different browsers and devices. The time is measured immediately after the cold page load.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Device, OS, browser&lt;/th&gt;
&lt;th&gt;Time to fingerprint&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MacBook Pro 2015 (Core i7), macOS 11, Safari 14&lt;/td&gt;
&lt;td&gt;5 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MacBook Pro 2015 (Core i7), macOS 11, Chrome 89&lt;/td&gt;
&lt;td&gt;7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acer Chromebook 314, Chrome OS 89&lt;/td&gt;
&lt;td&gt;7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel 5, Android 11, Chrome 89&lt;/td&gt;
&lt;td&gt;7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;iPhone SE1, iOS 13, Safari 13&lt;/td&gt;
&lt;td&gt;12 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pixel 1, Android 7.1, Chrome 88&lt;/td&gt;
&lt;td&gt;17 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Galaxy S4, Android 4.4, Chrome 80&lt;/td&gt;
&lt;td&gt;40 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MacBook Pro 2015 (Core i7), macOS 11, Firefox 86&lt;/td&gt;
&lt;td&gt;50 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Audio fingerprinting is only a small part of the larger identification process.
&lt;/h2&gt;

&lt;p&gt;Audio fingerprinting is one of the many signals our &lt;a href="https://github.com/fingerprintjs/fingerprintjs" rel="noopener noreferrer"&gt;&lt;span&gt;open source library&lt;/span&gt;&lt;/a&gt; uses to generate a browser fingerprint. However, we do not blindly incorporate every signal available in the browser. Instead we analyze the stability and uniqueness of each signal separately to determine their impact on fingerprint accuracy.&lt;/p&gt;

&lt;p&gt;For audio fingerprinting, we found that the signal contributes only slightly to uniqueness but is highly stable, resulting in a small net increase to fingerprint accuracy.&lt;/p&gt;

&lt;p&gt;You can learn more about stability, uniqueness and accuracy in our &lt;a href="https://fingerprintjs.com/blog/what-is-browser-fingerprinting/" rel="noopener noreferrer"&gt;&lt;span&gt;beginner’s guide to browser fingerprinting&lt;/span&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try Browser Fingerprinting for Yourself
&lt;/h3&gt;

&lt;p&gt;Browser fingerprinting is a useful method of visitor identification for a variety of anti-fraud applications. It is particularly useful to identify malicious visitors attempting to circumvent tracking by clearing cookies, browsing in incognito mode or using a VPN. &lt;/p&gt;

&lt;p&gt;You can try implementing browser fingerprinting yourself with our &lt;a href="https://github.com/fingerprintjs/fingerprintjs" rel="noopener noreferrer"&gt;&lt;span&gt;open source library&lt;/span&gt;&lt;/a&gt;. FingerprintJS is the most popular browser fingerprinting library available, with over &lt;code&gt;12K&lt;/code&gt; GitHub stars.&lt;/p&gt;

&lt;p&gt;For higher identification accuracy, we also developed the &lt;a href="https://fingerprintjs.com/" rel="noopener noreferrer"&gt;&lt;span&gt;FingerprintJS Pro API&lt;/span&gt;&lt;/a&gt;, which uses machine learning to combine browser fingerprinting with additional identification techniques. You can try FingerprintJS Pro free for &lt;code&gt;10&lt;/code&gt; days with no usage limits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get in touch
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Star, follow or fork our &lt;a href="https://github.com/fingerprintjs/fingerprintjs" rel="noopener noreferrer"&gt;&lt;span&gt;GitHub project&lt;/span&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Email us your questions at &lt;a href="mailto:oss@fingerprintJS.com"&gt;oss@fingerprintJS.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Sign up to our &lt;a href="https://mailchi.mp/708d84efc0c1/updates-signup" rel="noopener noreferrer"&gt;&lt;span&gt;newsletter&lt;/span&gt;&lt;/a&gt; for updates&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>typescript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Have you built a website with Gatsby?</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Mon, 04 Jan 2021 08:46:31 +0000</pubDate>
      <link>https://forem.com/savannahjs/have-you-built-a-website-with-gatsby-3okc</link>
      <guid>https://forem.com/savannahjs/have-you-built-a-website-with-gatsby-3okc</guid>
      <description>&lt;p&gt;Our team recently just rebuilt &lt;a href="https://fingerprintjs.com/"&gt;our website&lt;/a&gt; in Gatsby and set up our first blog using Netlify CMS. The main reason for the switch was for faster performance and easier content management, and in that regard its been great so far!&lt;/p&gt;

&lt;p&gt;Curious to hear your experiences using Gatsby and you have any tips and tricks for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How was your experience using Gatsby (overall, compared to other static site generators)?&lt;/li&gt;
&lt;li&gt;What CMS did you use (if any)?&lt;/li&gt;
&lt;li&gt;Are you doing for AB testing on your site? We had some trouble out of the box with standard tools (Google Optimize), so curious what others have found worked well for them!&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>gatsby</category>
    </item>
    <item>
      <title>The Beginner’s Guide to Browser Fingerprinting For Fraud Detection</title>
      <dc:creator>Savannah Copland 👋</dc:creator>
      <pubDate>Mon, 04 Jan 2021 08:17:13 +0000</pubDate>
      <link>https://forem.com/savannahjs/the-beginner-s-guide-to-browser-fingerprinting-4haa</link>
      <guid>https://forem.com/savannahjs/the-beginner-s-guide-to-browser-fingerprinting-4haa</guid>
      <description>&lt;p&gt;&lt;strong&gt;Website fraud can be incredibly frustrating to deal with, especially for small websites.&lt;/strong&gt; Fraud comes in many forms, including spam bots filling out forms, fraudsters trying to steal login information, or scammers making fake purchases. What website owners and developers need is the ultimate 'swiss army knife' for your fraud-fighting toolkit - browser fingerprinting.&lt;/p&gt;

&lt;p&gt;Browser fingerprinting provides a highly accurate user identifier that makes it much easier to triage suspicious traffic. The key to identifying those most likely to commit fraud is either by past activity, or by associating specific patterns of use with a higher likelihood of fraudulence.&lt;/p&gt;

&lt;p&gt;Browser fingerprinting is already used by many companies for developer-led fraud prevention as it cuts through spoofing attempts to accurately identify users, and it can do this without requiring additional permissions from the user. FingerprintJS has an open source &lt;a href="https://github.com/fingerprintjs/fingerprintjs"&gt;browser fingerprinting library&lt;/a&gt; with over 12K stars on Github and is used by 8,000+ websites. Fingerprinting techniques on their own have been found to be over &lt;a href="https://arstechnica.com/information-technology/2017/02/now-sites-can-fingerprint-you-online-even-when-you-use-multiple-browsers/"&gt;90% accurate&lt;/a&gt; in correctly identifying a unique user in the browser, and when used in conjunction with usage history, fuzzy matching, and probability engines, this accuracy can be further improved.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Fingerprinting Works
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Identifying a Vehicle
&lt;/h4&gt;

&lt;p&gt;To explain the technology in an 'ELI5' style, here's an analogy: let’s say you’re a detective in a large city trying to find one specific car suspected of being involved in a crime, as captured by a security camera. To find this car your plan is to go to a busy intersection and take note of all the details of passing cars until you find one that matches the vehicle on the security camera. Ideally, you would like to be able to uniquely identify the car, such that only one vehicle in the city matches your description, otherwise you may have to question multiple drivers.&lt;/p&gt;

&lt;p&gt;Let's say the security camera caught some basic details (or signals) about the vehicle. From this, you’ll be able to narrow your search considerably:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Color (blue)&lt;/li&gt;
&lt;li&gt;Manufacturer (Chevrolet)&lt;/li&gt;
&lt;li&gt;Type of car (truck)&lt;/li&gt;
&lt;li&gt;Model name (Silverado)&lt;/li&gt;
&lt;li&gt;Brand of tires (stock Goodyears)&lt;/li&gt;
&lt;li&gt;Age/year (2015-2021)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With these signals, you may be able to uniquely identify the vehicle right away, especially if any of the specifics are particularly rare. However, in a city with millions of drivers, there may be hundreds of blue Chevrolet Silverado trucks with standard-issue tires. The more standard the combination of signals, the harder it is to get a unique match.&lt;/p&gt;

&lt;p&gt;In those cases, you hope that your camera may have gotten lucky and matched on a more unique signal about the vehicle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wood panelling&lt;/li&gt;
&lt;li&gt;Custom logo or paint job&lt;/li&gt;
&lt;li&gt;Rust or damage&lt;/li&gt;
&lt;li&gt;Interior decorations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any one of these signals may quickly narrow down your search. A blue Chevrolet Silverado truck with a local company’s logo could very well be unique, even in a large city.&lt;/p&gt;

&lt;p&gt;It's worth mentioning the most uniquely identifiable element of a car that I have let out so far - the license plate. License plates serve the express purpose of uniquely identifying a car, but what good will they do if the owner removes their plates or swaps them with fakes? It’s important to have a backup for when this method of identification fails.&lt;/p&gt;

&lt;p&gt;By assembling a broad and comprehensive set of identifiers you can narrow the list of suspects to make singling out a bad-actor much easier.&lt;/p&gt;

&lt;h4&gt;
  
  
  Identifying a Visitor
&lt;/h4&gt;

&lt;p&gt;Fingerprinting works just about exactly the same as the car example above. Only now you are trying to identify a visitor to a website (suspect) by capturing signals passed via the visitor’s browser (car) using a fingerprinting function (security camera).&lt;/p&gt;

&lt;p&gt;A lot of signals can be captured through the browser, including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User agent details (browsers installed and their versions, operating system)&lt;/li&gt;
&lt;li&gt;Hardware details (screen resolution, battery usage, device memory)&lt;/li&gt;
&lt;li&gt;Browser plugins used&lt;/li&gt;
&lt;li&gt;Browser and OS settings&lt;/li&gt;
&lt;li&gt;WebGL parameters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a new visitor lands on your webpage, the fingerprinting function collects signals and compiles them into a hash that can be stored. Any time this visitor returns, their fingerprint can be compared to past visit history to identify suspicious behavior or fraudulent activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accuracy
&lt;/h2&gt;

&lt;p&gt;Let's say you are now collecting a 'fingerprint' for every visitor to your website. For that  fingerprint to be useful as a method of uniquely identifying visitors, it needs to have a high accuracy. The FingerprintJS Pro API has a 99.5% accuracy rate, which means for every 1,000 visits, 995 are correctly associated with a unique identifier.&lt;/p&gt;

&lt;p&gt;For the 5 out of 1,000 that are not correctly identified, they are either false positives or false negatives:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;False positive: multiple unique visitors are given the same fingerprint&lt;/li&gt;
&lt;li&gt;False negative: one visitor over multiple visits are given different fingerprints&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To reduce false results, your fingerprint should use the right combination of signals that balance both uniqueness and stability. If a signal is highly unique, it will reduce your chances of a false negative, whereas a signal that is highly stable will reduce your chances of a false positive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Tysg6pa0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/u5gtm746eop7y8lkqe41.PNG" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Tysg6pa0--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/i/u5gtm746eop7y8lkqe41.PNG" alt="Uniqueness versus stability"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While there are hundreds of signals available via the browser, you may want to avoid using some signals in your fingerprinting function altogether. If a signal has both low uniqueness and low stability, it is likely to change over time or be spoofed frequently, and would not contribute meaningfully to uniqueness. To our car example, this might be whether a car has a dirty windshield - you cannot count on this signal to improve your chances of finding the correct car. In the world of browser fingerprinting, current battery level is a poor signal, and so while it is accessible, I would not recommend including it in any fingerprinting function you use.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Case for Cookies
&lt;/h2&gt;

&lt;p&gt;Special consideration should be given to highly unique identifiers that are not always available for user identification purposes. The most ubiquitous example of this is cookies. &lt;/p&gt;

&lt;p&gt;Cookies work by storing a unique identifier hash in the browser when a visitor first lands on your website. When a visitor has a cookie that matches a previous visit record in your database, you can be certain that these two visitors are the same. However, cookies are a very easy identifier for a visitor to conceal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cookies can be cleared in browser settings&lt;/li&gt;
&lt;li&gt;Adblockers can disable cookies by default&lt;/li&gt;
&lt;li&gt;Visitors can revoke consent to being cookied as part of GDPR or CCPA&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In these cases, instead of including a cookie as an identifier in your fingerprinting function, it can be more useful to use logic to determine when to use cookies as your identifier:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If cookie matches a previous record: use cookie&lt;/li&gt;
&lt;li&gt;If no cookie matches previous record: use fingerprint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One of the main advantages of fingerprinting is that it is stateless. A well-implemented fingerprint can remain stable through multiple sessions, incognito browsing, uninstalling or reinstalling apps, or clearing cookies. For that reason, using the two methods in conjunction with one another can give a higher % accuracy than either identification method alone.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;FingerprintJS Pro achieves its high rate of accuracy by using fingerprinting, cookies and additional machine learning techniques that incorporate IP address and geolocation.&lt;/em&gt; One challenge is keeping up with changes in available signals as new browser versions are released. Anytime Chrome or Safari is updated, for example, identification techniques need to be re-evaluated to determine if further tweaks need to be made to keep accuracy high. The team at FingerprintJS is constantly looking to improve our accuracy by iterating on the signals, algorithms, and techniques used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fraud Applications For Fingerprinting
&lt;/h2&gt;

&lt;p&gt;An important thing to keep in mind when dealing with fraud is that only a small percentage of visitors are responsible for the &lt;a href="https://www.f5.com/labs/articles/threat-intelligence/2020-phishing-and-fraud-report#_fig19"&gt;majority of fraud cases&lt;/a&gt;. You will need to find ways to isolate these fraudulent visitors, verify their identity through authentication, and blacklist them as needed. However, you will want to avoid putting up roadblocks for your ‘trusted’ traffic, as additional authentication can be detrimental to user experience. You don't want to be slowing your users’ ability to access their account, make purchases, and engage with your website. &lt;/p&gt;

&lt;p&gt;Let's explore one example of online fraud to see how you could use fingerprinting in a flexible way to isolate fraud and keep your website experience seamless.&lt;/p&gt;

&lt;p&gt;Account takeover is a common form of fraud where malicious users try to log in to other users’ accounts, and is an excellent use-case for fingerprinting technology. Additional security at login can make account takeover much more difficult, though the type of authentication used may depend on the suspicious behavior your website experiences most often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;em&gt;For bot or brute force attacks&lt;/em&gt; (one user or a network of bots trying many combinations of usernames/passwords):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Show a captcha after 1 unsuccessful login attempt on a fingerprint.&lt;/li&gt;
&lt;li&gt;Lock user out of attempting login after 5 unsuccessful attempts on a fingerprint.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;li&gt;

&lt;p&gt;&lt;em&gt;For phished accounts&lt;/em&gt; (a user obtained someone else’s legitimate login information through a scam or social engineering):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Require two-factor or email authentication when attempting to login with a new fingerprint.&lt;/li&gt;
&lt;li&gt;Blacklist specific fingerprinted visitors from your site based on their fingerprint.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For each of these cases, type of authentication needed can be incorporated into your website by using existing workflows without having to fundamentally change the architecture of your site.&lt;/p&gt;

&lt;p&gt;It is also important to note that users intending to commit fraud are much more likely to use techniques to conceal their identity, including using incognito mode, VPNs, and disabling cookies. These are the cases where fingerprinting especially shines, as it can associate these users without needing easily concealed identifiers like cookies and IP addresses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser vs. Device Fingerprinting
&lt;/h2&gt;

&lt;p&gt;The FingerprintJS open source library as well as the Pro API are intended for browser fingerprinting - they can accurately identify visitors to a website using all modern mobile and desktop browsers. However, if you want to identify users of a native mobile app, you will need to use a device fingerprinting function that is made specifically for each mobile operating system. The signals available for mobile app developers are different from signals that can be retrieved in the browser, and vary between iOS, Android, and other mobile operating systems.&lt;/p&gt;

&lt;p&gt;The FingerprintJS team recently launched &lt;a href="https://github.com/fingerprintjs/fingerprint-android"&gt;Fingerprint Android&lt;/a&gt;, our first open source library for identifying unique Android devices. You can read more about how our Fingerprint Android library works in our &lt;a href="https://fingerprintjs.com/blog/device-fingerprinting-android/"&gt;explainer article&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Involved
&lt;/h2&gt;

&lt;p&gt;I would love to hear your questions and get feedback from the developer community on our fingerprinting technology.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here are a few ways you can get involved&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Star, follow or fork our Github projects: &lt;a href="https://github.com/fingerprintjs/fingerprintjs"&gt;FingerprintJS&lt;/a&gt; (browser fingerprinting) and &lt;a href="https://github.com/fingerprintjs/fingerprint-android"&gt;Fingerprint-Android&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Need more accurate browser fingerprinting for your business? Try FingerprintJS Pro for &lt;a href="https://fingerprintjs.com/"&gt;99.5% fingerprinting accuracy&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="//mailto:sales@fingerprintjs.com"&gt;Email us&lt;/a&gt; your questions&lt;/li&gt;
&lt;li&gt;Sign up for our &lt;a href="https://mailchi.mp/708d84efc0c1/updates-signup"&gt;newsletter&lt;/a&gt; for updates&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
