Detecting if a browser is using Private Browsing mode

I'm building an extranet for a company paranoid about security. They want to make sure that (among other things) their users are browsing the site with the Private Browsing mode switched on in their web browser so that no cookies or history is kept.

I found only this http://jeremiahgrossman.blogspot.com/2009/03/detecting-private-browsing-mode.html and https://serverfault.com/questions/18966/force-safari-to-operate-in-private-mode-and-detect-that-state-from-a-webserver

The ideal solution would use no or minimal javascript. Would attempting to set a unique cookie work for all browsers and platforms? Anyone done this before?

thanks!


update

http://crypto.stanford.edu/~collinj/research/incognito/ uses the CSS visited technique of the browser fingerprinters mentioned by other posters- thanks for the hints.

I like it because it is small and elegant, but still want to be able to do it without javascript if possible.


Update June 2019

Google is removing the ability to detect Private Browsing Mode permanently in Chrome 76 onwards. So, if you're wanting to detect private browsing it's now impossible (unless you find a way to do it that Google hasn't found). The ability to detect private browsing mode has been acknowledged as a bug and was never intended.

To anyone else coming across this question, please note as of 2014, there is no reliable or accurate way to detect if someone is browsing in an incognito/private/safe browsing mode through Javascript or CSS. Previous solutions that once worked like the CSS history hack have since been rendered unusable by all browser vendors.

There should never be a situation where needing to detect private browsing mode on a normal day-to-day website is ever needed. People are choosing to browse anonymously and or not anonymously for their own reasons.

Browsers like Chrome and Firefox do not disable functionality like localStorage any more. They simply namespace it in a temporary location to prevent websites that use it from erroring out. Once you're finished browsing, the namespace is erased and nothing is saved. If you are testing for localStorage support regardless of mode, it will always return true for browsers that support it.

Other means of detecting private mode in Chrome specifically have been completely patched and will no longer work.

If it is required internally by a company, you should develop a browser plugin. Chrome and Firefox, in particular, expose internal API's which allow plugins to check if the user is in private browsing/incognito mode and action accordingly. It cannot be done outside of a plugin.


Here's an easier way to do detect privacy mode. This works in Safari only. I created it because a web app I am developing uses localStorage. LocalStorage is not available in Safari when in privacy mode, thus my app will not work. On page load, run the script below. It shows an alert box if we cannot use localStorage.

try {
  // try to use localStorage
  localStorage.test = 2;        
} catch (e) {
  // there was an error so...
  alert('You are in Privacy Mode\nPlease deactivate Privacy Mode and then reload the page.');
}

Current state

Google Chrome has developed further and leaves no more space for detection when using incognito mode. Same might apply for other browsers.


Old solutions (might partially work)

It is possible to detect enabled private browsing modes for the majority of used browsers. This includes Safari, Firefox, IE10, Edge and Google Chrome.


Firefox

When the private browsing mode of Firefox is enabled, the IndexedDB throws an InvalidStateError because it is not available in private browsing mode.

To very if that:

var db = indexedDB.open("test");
db.onerror = function(){/*Firefox PB enabled*/};
db.onsuccess =function(){/*Not enabled*/};

Safari

For Safari, the key is the local storage service. It is disabled in privacy mode. So try to access it and use a try-catch clause. The following method works on both, OSX and iOS devices. Credits for this method are going to this question and answer

var storage = window.sessionStorage;
try {
    storage.setItem("someKeyHere", "test");
    storage.removeItem("someKeyHere");
} catch (e) {
    if (e.code === DOMException.QUOTA_EXCEEDED_ERR && storage.length === 0) {
        //Private here
    }
}

IE10/Edge

Internet Explore is even going to disable the IndexedDB when in privacy mode. So check for existence. But that's not sufficient enough, because older browsers maybe don't even have an IDB. So do another check, e.g. for events that only IE10 and subsequent browser have/trigger. A related question on CodeReview can be found here

if(!window.indexedDB && (window.PointerEvent || window.MSPointerEvent)){
 //Privacy Mode
}

Chrome

Update: This doesn't work since Chrome 76 (thanks to @jLynx)

Chromes Incognito mode can be verified by the file system. A great explanation can be found here on SO

var fs = window.RequestFileSystem || window.webkitRequestFileSystem;
if (!fs) {
    console.log("FS check failed..");
    return;
}

fs(window.TEMPORARY, 100, function (fs) {}, function (err) {
//Incognito mode
});

Chrome 83 arrives with redesigned security settings, third-party cookies blocked in Incognito by default!

So this one is easy, create a iframe to a third party site, have it send a postMessage back notifying you if navigator.cookieEnabled is true or false. Ofc users have the option to disable third party cookie as well. So i tested and disabled 3th party cookies in the settings. But it still said cookie was enabled on third-party iframes using navigator.cookieEnabled. it only became disabled once i used Incognito - perhaps a bug?

new Promise((rs, rj, m = new MessageChannel(), d = document, i = d.createElement('iframe')) => {
  i.src = 'https://httpbin.org/base64/PHNjcmlwdD5vbm1lc3NhZ2UgPSBlID0+IGUuZGF0YS5wb3N0TWVzc2FnZShuYXZpZ2F0b3IuY29va2llRW5hYmxlZCk8L3NjcmlwdD4='
  i.onload = _ => i.contentWindow.postMessage(m.port1, '*', [m.port1], m.port2.onmessage = e => i.remove(rs(e.data)))
  i.hidden = 1
  d.body.append(i)
}).then(thirdPartyCookieEabled => 
  console.log('Third party cookie enabled:', thirdPartyCookieEabled)
)

You could also probably do it using only js + ajax but didn't want to set up a 2 servers to test it myself. but for this SameSite=none have to be set as well.

res = await fetch('https://httpbin.org/cookies/set?enabled=1', {
  credentials: 'include' 
})
json = await res.json()
console.log(!!json.cookies.enabled)

Here is my take on detecting private mode

function detectPrivateMode(cb) {
    var db,
    on = cb.bind(null, true),
    off = cb.bind(null, false)

    function tryls() {
        try {
            localStorage.length ? off() : (localStorage.x = 1, localStorage.removeItem("x"), off());
        } catch (e) {
            // Safari only enables cookie in private mode
            // if cookie is disabled then all client side storage is disabled
            // if all client side storage is disabled, then there is no point
            // in using private mode
            navigator.cookieEnabled ? on() : off();
        }
    }

    // Blink (chrome & opera)
    window.webkitRequestFileSystem ? webkitRequestFileSystem(0, 0, off, on)
    // FF
    : "MozAppearance" in document.documentElement.style ? (db = indexedDB.open("test"), db.onerror = on, db.onsuccess = off)
    // Safari
    : /constructor/i.test(window.HTMLElement) || window.safari ? tryls()
    // IE10+ & edge
    : !window.indexedDB && (window.PointerEvent || window.MSPointerEvent) ? on()
    // Rest
    : off()
}

detectPrivateMode(function (isPrivateMode) {
    console.log('is private mode: ' + isPrivateMode)
})

edit found a modern, faster, synkronas way to try it in firefox (they don't have service workers in privat mode) similar to ie don't include indexedDB but the test only works in secure sites

: "MozAppearance" in document.documentElement.style ? navigator.serviceWorker ? off() : on()

There's no way for your web page to know, absolutely for sure, that the user is in private browsing mode. Any attempts to check for various browser features will need to change often as security implementations are updated. It may work for some time in some browsers, but not all.

If the company is that concerned about security, I'd suggest rolling your own Firefox or Chromium distribution with locked down privacy settings, and only allowing that custom client to connect to the extranet.