‹ Jack's Brain

(Ab)using Getters for DevTool Open Detection

May 27, 2020

I recently stumbled upon a site I wanted to save a video from, so I went to open devtools and check the Network tab for the video source when suddenly the page redirected me to an error page! After a couple tries it was clear this was detecting devtools being opened and directing me away from the page — how curious (and annoying).

A quick google indicated that there are some hacks to detect sudden screen width changes as indicator of the devtools pane sliding open, but an undocked pane gave the same result. After some hunting via the initiator, this was the code responsible:

if ((navigator.userAgent.indexOf("Chrome") != -1 || navigator.userAgent.indexOf("Safari") != -1 || navigator.userAgent.indexOf("MSIE") != -1 || navigator.userAgent.indexOf("coc_coc_browser") != -1)) {
    var checkStatus;
    var element = new Image();
    Object.defineProperty(element, 'id', {
        get: function() {
            checkStatus = 'on';
            throw new Error("Dev tools checker");
        }
    });

    setInterval(function check() {
        checkStatus = 'off';
        console.dir(element);
        if (checkStatus == 'on') {
            window.location.href = "/error.html";
        }
    }, 1000);
}

Let’s break this into two functional blocks. We’re gated by a user agent check to detect browsers that actually have devtools, and then we dive in to the meat. Let’s go block by block:

var checkStatus;
 var element = new Image();

Easy peasy; we’re declaring checkStatus and instantiating an HTMLImageElement object. What do we do with them?

Object.defineProperty(element, 'id', {
    get: function() {
        checkStatus = 'on';
        throw new Error("Dev tools checker");
    }
});

I don’t spend too much time on the front end anymore, so I had to go look up Object.defineProperty. MDN to the rescue! defineProperty is basically the same as regular property definition (myObj.foo = 'bar') but with a number of extra possibilities within the third descriptor parameter. The descriptor is an object that defines attributes such as whether a property should be read-only/un-deletable/etc. The attribute that we care about and is in use here is get, a property containing a getter function that is called to retrieve the value of the property.

So what this block is doing is defining a property called id and providing a getter for it that sets the checkStatus variable to on and then throws an error. So, any time this Image‘s id property is accessed, checkStatus is set to on. How does this detect when devtools are opened? Let’s look at the next block.

setInterval(function check() {
    checkStatus = 'off';
    console.dir(element);
    if (checkStatus == 'on') {
        window.location.href = "/error.html";
    }
}, 1000);

Once a second, we set checkStatus to ‘off’, and then we call console.dir. This was another new one for me — console.dir is basically console.log but with an interactive tree of object properties. When we console.dir(element), we build that interactive tree, and, critically, devtools gets all the object properties to display — including the id object that was defined and sets the checkStatus flag to ‘on’ when the property is accessed. Because that getter updates the variable, we catch it on the next line and redirect with window.location.href. Sneaky!

So, to summarize, we create an object with a getter which will change a variable when accessed, then run console.dir which, when devtools is open, will access all properties of an object, which will run the getter, which will update the variable which we can then check and do whatever we want.

So how can we circumvent this? Well the, cheeky thing to do is open devtools and quickly (within our one second timeout) paste console.dir = null into the console which overwrites the dir function with nothing. There may be neater ways to do so; I’d love to read them in the comments!