Incognito Mode Detection: Detecting Visitors Who Browse in Private Mode

July 29, 2021
July 29, 2021
Incognito mode detection

Browsers attempt to ensure a user’s privacy by offering Incognito mode, which allows the user to surf the internet without worrying about their history, cookies, or information being saved permanently. However, in many business use cases, this can end up being harmful. For example, visitors may use Incognito mode to gain unlimited content access, bypassing undetected paywalls.

We’ll review four methods of detecting visitors using Incognito mode and discuss which browsers support these methods. Ultimately, we’ll compare these methods to Fingerprint, making it all easier.

Method 1: Access Timings

Jesse Li discovered this method. When the browser is in Incognito mode, the Filesystem API writes faster in Chrome because Chrome uses a temporary filesystem with a limited storage quota of 120 MB.

To test this, first create an array of randomly generated large strings:

const strings = [
    's885D7fqH+wKRJoHZ5duaBmhRnlYy7ZgqWA+h14y44J...',
    'Kh6WK6D/xRhYfLksJqlG5Sbu8zsK445TpB8....',
    'AwQ5MBmwxUdYZveqq66TtzoXS9Jn3l9OqVPM7eEukx+nACtQtj8GHv1TokzTWNYF6....',
    'Ck+4tAo/xKe8b0U+JlMNiccJvJ8/+/c3c+bgrW0KAXCxA....',
    'YKPQ1Tv9E42vNMaS+1q2DETAaoUMDQdOgK4W4slHFJ+itD0lwE4eOJ/8rV4Igal....'
]

They have been truncated here for simplicity. These strings are 5000 characters long, and generated using the following command:

base64 /dev/urandom -w 0 | head -c 5000

These strings will be used to write files. Also, declare the following variables:

const SIZE = 6*1024*1024,
    NB_TIMINGS = 100,
    NB_WRITES_ITERATIONS = 200

SIZE will be used to determine the storage space allocated by your website. NB_TIMINGS is the number of times you’ll run the writing files code, and NB_WRITE_ITERATIONS is the number of times you’ll write each file.

Next, use window.webkitRequestFileSystem, which lets you gain access to a sandboxed filesystem:

window.webkitRequestFileSystem(window.TEMPORARY, SIZE, onInit)

The first parameter this function takes is type, which can either be window.TEMPORARY or window.PERSISTENT. Since you’re testing the temporary filesystem storage, use window.TEMPORARY. The second parameter is the storage size you need to be allocated, and the third is a callback function (which you’ll define in a bit) that will accept the filesystem instance.

Next, define the onInit function, which will loop for NB_TIMINGS (which you declared above) times and calls the function writeFiles (which you’ll define shortly). This passes it the filesystem instance that window.webkitRequestFileSystem passes to the callback, which in this case is onInit. Then push the time it takes into a timing array (which you’ll see once execution is done):

const onInit = async (filesystem) => {
    const timings = []
    for (let i = 0; i < NB_TIMINGS; i++) {
        timings.push(await writeFiles(filesystem));
    }
    document.getElementById('timings').innerText = timings.join(",")
}

Define the writeFiles function, which will loop for NB_WRITES_ITERATION times, then loop over the large strings in the strings array, and call the writeFile function (which you will declare after), passing it the filesystem instance and the string in each iteration.

The function will return the time it took for this process to happen:

const writeFiles = async (filesystem) => {
    const time = new Date() // time before starting writing
    for (let i = 0; i < NB_WRITES_ITERATIONS; i++) {
        for (let j = 0; j < strings.length; j++) {
            await writeFile(filesystem, strings[j]);
        }
    }
    return new Date() - time // time after writing the files
}

Finally, define the writeFile function that will use the filesystem instance to write the string into a file:

const writeFile = (filesystem, data) => {
    return new Promise((resolve) => {
        filesystem.root.getFile('data', {create: true}, (fileEntry) => {
            fileEntry.createWriter((fileWriter) => {
                fileWriter.onwriteend = resolve
                
                var blob = new Blob([data], { type: 'text/plain' })
                fileWriter.write(blob)
            })
        })
    })
}

This will be your script. To make sure you see the output in the page, add a div element with the ID timings:

<div id="timings"></div>

This is where the array of timings in the onInit function will be displayed, which includes the time to write each file.

Testing on Chrome

Here’s a portion of the output on Chrome without Incognito mode:

Testing on Chrome without Incognito - Method 1

And here’s a portion of the output on Chrome using Incognito mode:

Testing on Chrome with Incognito - Method 1

As you can see, the numbers dropped tremendously when the browser was using Incognito mode.

This has been known since 2019 on Chrome 76, but it still works now on Chrome 89.

Testing on Edge

When testing this method on Edge, it produced the same behavior.

When the browser was not Incognito:

Testing on Edge without Incognito - Method 1

When the browser was in Incognito (or InPrivate on Edge):

Testing on Edge with Incognito - Method 1

As Edge is now using Chromium for its engine, it can be assumed that as long as this method works on Chrome, it will also work on Edge.

Testing on Other Major Browsers

Testing this method on other major browsers like Firefox, Safari, and Opera will not work, as the requestFileSystem function is not supported.

Furthermore, this method, even with Chrome and Edge is not optimal. Compared to a visitor in Incognito mode and one not, it will likely require a lot of estimation.

Also, the same behavior pertained after repeated testing on Chrome, but the result decreased significantly for both Incognito and non-Incognito modes, and the browser behaved oddly afterward. Therefore, subjecting your visitors to a similar method would not be a good idea.

Method 2: Filesystem Quotas

Another method to detect whether the user is in Incognito mode or not is the StorageManager API method estimate. This method estimates how much storage the website uses and how much is available for it to use.

The code for this method is simple. If the estimated storage is less than 120 MB, then the user is in Incognito mode.

You can test this by adding the following code to a script:

navigator.storage.estimate()
    .then(({usage, quota}) => {
        document.getElementById('answer').innerText = quota < 120000000 ? 'Yes' : 'No'
    })

Then, in the HTML, add the element with ID answer to see the result:

<div id="answer"></div>

If the user is in Incognito mode, the element answer should have the text Yes. Otherwise, it should be No.

Testing on Chrome

It was reported that this method worked beginning with Chrome 74. However, it no longer works after Chrome 84.

Testing this script on Chrome 89, without Incognito mode it gave the following result:

Testing on Chrome without Incognito - Method 2

And the same result was given with Incognito mode:

Testing on Chrome with Incognito - Method 2

Testing on Firefox

This issue was never reported on Firefox before. After testing, it gave the same result as when not using Incognito mode:

Testing on Firefox without Incognito - Method 2

And when using Incognito (or Private) mode:

Testing on Firefox with Incognito - Method 2

Both show the answer No, which means this method cannot be used to detect Incognito mode on Firefox.

Testing on Other Browsers

This method also did not work on Edge. On Safari, navigator.storage.estimate it is not supported.

This method is outdated and should not be used, as it does not work on any current browser. It only works for users on Chrome versions 74–84.

Method 3: IndexedDB API

The IndexedDB API is used for storing large data like files and blobs. This method is simple. If IndexedDB is available, the browser is not in Incognito mode. This was first detected on Firefox 60.

More specifically, if the method indexedDB.open does not throw an error, the browser is not using Incognito mode. You can test this by creating a script with the following code:

const answerElm = document.getElementById('answer'), 
    db = indexedDB.open('test')
db.onerror = function () {
    //if an error is thrown, Incognito mode
    answerElm.innerText = 'Yes'
}
db.onsuccess = function () {
    //if not error is thrown, not Incognito mode
    answerElm.innerText = 'No'
}

This uses indexedDB.open and attaches an event handler for onerror and onsuccess. If an error occurs, then the browser is using Incognito mode.

Testing on Firefox

Testing this script on Firefox 88, you can see the following result using Incognito mode:

Testing on Firefox without Incognito - Method 3

And the following result is not in Incognito mode:

Testing on Firefox with Incognito - Method 3

As we can see, this method still works on Firefox 88. When the user is in Incognito mode, the answer is Yes. When the user isn’t, the answer is No.

Testing on Chrome

On Chrome 89, we got No when not using Incognito mode:

Testing on Chrome without Incognito - Method 3

And the same result when Incognito mode was used:

Testing on Chrome with Incognito - Method 3

This means that this method does not work on Chrome. There were no reported incidents that showcased that it ever worked on Chrome.

Testing on Safari

When testing on Safari 14 on iOS, you get the same result when not in Incognito mode:

Testing on Safari without Incognito - Method 3

And with Incognito:

Testing on Safari with Incognito - Method 3

This means that this method does not work on Safari. There are no previous incidents that showcase that it ever did.

Although this method is simple, it’s not enough as it only detects Incognito mode on Firefox, meaning users on other major browsers can still go undetected.

Method 4: Local Storage

This method was detected in Safari before version 11. In Incognito or Private mode, Safari disabled the local storage.

You can test this by creating a script with the following code:

const answerElm = document.getElementById('answer')
try {
    localStorage.setItem('test', 'incognito')
    localStorage.removeItem('test')
    //no error thrown, not incognito mode
    answerElm.innerText = 'No'
} catch (e) {
    if (e.code === DOMException.QUOTA_EXCEEDED_ERR && localStorage.length === 0) {
        //Incognito mode
        answerElm.innerText = 'Yes'
    } else {
        //not Incognito mode. Error thrown for some other reason
        answerElm.innerText = 'No'
    }
}

localStorage.setItem tests if the localStorage is working. If it is, then the user is not in Incognito mode. If an error is thrown and the type of error is DOMException.QUOTA_EXCEEDED_ERR, and the length of local storage is 0, then the user is in Incognito mode. Otherwise, the error thrown could be for some other reason.

Again, make sure to add the following element in the HTML document to see the result:

<div id="answer"></div>

Testing on Safari

When testing this method on Safari 14 on iOS without using Incognito mode, this is the result:

Testing on Safari without Incognito - Method 4

And the same result for Incognito mode:

Testing on Safari with Incognito - Method 4

This method does not work on Safari anymore and was not reported to work on any other major browser.

Method 5: Fingerprint

After going through all the previous methods and experiencing their limitations, let’s look at Fingerprint. Fingerprint Promises "99.5% accurate browser fingerprinting,” which will allow you to detect your visitors’ true identity and protect your content.

Fingerprint is helpful in many use cases, including:

  • Prevent account fraud or fake accounts by ensuring all your visitors are real.
  • Making sure all payments are made securely, preventing fraudulent payments.
  • Protecting your content, making sure users don’t bypass your paywall.

How It Works

Create a 14-day free trial. Enter your email to begin the onboarding process, entering some information like your website name. Accept the terms and conditions, and finally, you will be given a code snippet to add to your website.

Code Snippet from Fingerprint

This snippet loads the Fingerprint script to your webpage from the CDN. Once it’s loaded, you can start using Fingerprint with the API key given to your account (it’s part of the code in the snippet).

Before adding this code to your website, check your email and verify your email address with Fingerprint first. This is necessary for the subscription to take effect.

Once you’ve verified your email, go ahead and add this script to any of your web pages. If everything is correct, the same page that showed you the code snippet should take you to the dashboard:

Fingerprint dashboard

You can see that one user is already detected and that user is you!

We can pass the "extendedResult" option in the "get" method to return details about the visitor, including whether they are using Incognito or not.

This will be the code snippet now:

function initFingerprintJS() {
	FingerprintJS.load({ apiKey: 'fNmQOkVpWOuulOZhhYuv' })
		.then(fp => fp.get({extendedResult: true}))
		.then(result => {
                //check if incognito was detected
		document.getElementById('answer').innerText =
		result.incognito ? 'Yes' : 'No'
		})
		.catch(err => console.error(err))
}

The response should include multiple parameters, including browser and device details if everything is correct. For our purposes, we only need to focus on two parameters:

  1. visitorID: The ID of the visitor you just passed.
  2. incognito: A true or false value that shows whether a visitor uses incognito mode. If the user is in Incognito mode, its value will be true, otherwise false.

Finally, to see the “answer,” add the following in the HTML:

<div id="answer"></div>

Now, you’re ready to detect whether visitors are using Incognito mode or not.

Let’s test this on different major browsers. Also, you can test using the live version deployed here.

Testing on Chrome

Using Chrome 89, Fingerprint was able to detect when not using Incognito:

Testing on Chrome without Incognito - Method 5

And when using it:

Testing on Chrome with Incognito - Method 5

Testing on Firefox

Using Firefox 88, Fingerprint was able to detect when not using Incognito:

Testing on Firefox without Incognito - Method 5

And when using it:

Testing on Firefox with Incognito - Method 5

Testing on Edge

Using Edge 90, Fingerprint was able to detect when not using Incognito:

Testing on Edge without Incognito - Method 5

And when using it:

Testing on Edge with Incognito - Method 5

Testing on Opera

Using Opera 76, Fingerprint was able to detect when not using Incognito:

Testing on Opera without Incognito - Method 5

And when using it:

Testing on Opera with Incognito - Method 5

Testing on Safari

Using Safari 14 on iOS, Fingerprint was able to detect when not using Incognito:

Testing on Safari without Incognito - Method 5

And when using it:

Testing on Safari with Incognito - Method 5

Browser Support

Fingerprint supports the following browser versions:

  • Internet Explorer 11
  • Edge 93+
  • Chrome 49+
  • Firefox 52+
  • Desktop Safari 12.1+
  • Mobile Safari 10.3+
  • Samsung Internet 14.1+
  • Android Browser 4.4+
  • Brave all versions

For an up-to-date list of currently supported versions, visit this link.

Some old browsers like IE11 and Android Browser 4.1 will need a Promise polyfill before using it.

Comparing This Method to Previous Ones

Using Fingerprint is a better solution than previous methods. First, setting up Fingerprint and adding the code to your website is a five-minute process. It’s straightforward and does not rely on “quirks” or different methods based on which browser the user might be using.

Second, Fingerprint is the only method capable of detecting Incognito mode on all major browsers. All the other techniques detect Incognito mode only on some browsers, and most are outdated and don’t work anymore.

Not only is Fingerprint easy to integrate, but you can use it in different ways. For example, you used the CDN method above, but you can also install it with NPM or use it with RequireJS. This makes it a flexible solution for whatever kind of architecture you have for your project.

Conclusion

In recent years, browsers have been making it harder to detect Incognito mode, making it a perpetual task to ensure your website’s detection methods are up to date and can still detect a visitor’s identity. Unfortunately, it will only continue to become harder to find a method that accurately detects the visitor.

Fingerprint is the optimal solution to keep businesses safe from visitors using Incognito mode to access limited content.