A primer on the extension-identifying fingerprinting vectors in our study

We describe in detail the fingerprinting vectors that we study and show how these vectors could be used to fingerprint extensions in the wild.

We study five different behavior exhibited by Chrome and Firefox extensions, as follows:

  1. Use of Global APIs through Injected Scripts:
    An extension may inject a piece of JavaScript code into the DOM of the visited page, either through the executeScript API from the background, or through the document.createElement API within the content scripts. The injected code executes directly into the global JavaScript namespace of the Web page, controlled by the page itself, unlike the content scripts which has its isolated JavaScript namespace.

    Due to this lack of namespace isolation between the injected code and the Web page JavaScript, the extension could be exposed to being fingerprinted. This is because an adversarial website may overwrite the native definitions of the global APIs in its own namespace and can thus control their invocation by any external code that uses them, as follows:
    // injected.js
    var foo = [1,2,3,4,5];
    foo.forEach((element)=>{console.log(element)});
    
    // webpage-script.js
    function __hook(object, property, tag) {
        let __originalFunc = object[property];
        function __customFunc() {
            // Extracting API related information.
            let context = this;
            let args = Array(...arguments);
            while (arguments.callee.caller) {
                // Extracting the source code of the executing code.
            }
        // Capturing the stack trace of the executing code.
        let stacktrace = new Error().stack;
        reportToServer({ api, context, args, stacktrace, callerData });
        // Now, returning the result from executing native function.
        return __originalFunc.apply(this, arguments);
      }
      object[property] = __customFunc;
    }
    __hook(Array.prototype, "forEach", "Array.forEach");
    
    In the above example, when the extension-injected code is executed (i.e. the foo.forEach invocation), it allows the Web page to capture extesion-specific operation through API overwrites and, thus, infer the presence of certain extensions.
  2. Registering variables in the Global JavaScript Namespace:
    Even if a script injected by the extensions does not invoke any JavaScript API, as discussed above, they could still be fingerprintable by the side-effects that may be caused by polluting the global JavaScript namespace. That is, if an extension-injected script writes a global variable to the window object, this could still be used to identify the extension who set it, as follows:
    // injected.js
    window.foo = "bar";
    
    //webpage-script.js
    for (let v in Object.getOwnPropertyNames(window)) {
        if (isVariableSeen(v) || isVariableSeen(window.v)) {
            reportToServer("Target extension is installed!");
        }
    }
    Important: Please note that since the set of default global variables on the window object may change over time based on different browser vendors and versions, our tests may incorrectly classify an extension as fingerprintable. In such cases, we ask developers to visit our page once without the extension and extract the list of global variables available by default, through Object.getOwnPropertyNames(window) and compare against the variables detected in our tests, available at window.top.variables.
  3. Cookies:
    An extension may need to add, drop or manipulate existing cookies on certain websites to enable desired functionalities. However, this is also accessible to the Web page either through the document.cookie API on the client side or even within the headers on the server-side.
    // contentScript.js or injected.js
    document.cookie = "extension-foo=bar";
    
    // webpage-script.js
    for (let cookie of document.cookie.split(";")) {
        if (isCookieSeen(cookie)) {
            reportToServer("Target extension is installed!");
        }
    }
  4. Storage-based Interactions:
    An extension is susceptible to be fingerprinted by websites if it stores data on the client side using any of the following APIs: localStorage, sessionStorage and IndexedDB.
    // contentScript.js or injected.js
    window.localStorage["foo"] = "bar";
    window.sessionStorage.bat = "baz";
    
    // webpage-script.js
    for (let key of Object.getOwnPropertyNames(window.localStorage)) {
        if (isStorageSeen(key) || isStorageSeen(window.localStorage[key])) {
            reportToServer("Target extension is installed!");
        }
    }
    for (let key of Object.getOwnPropertyNames(window.sessionStorage)) {
        if (isStorageSeen(key) || isStorageSeen(window.sessionStorage[key])) {
            reportToServer("Target extension is installed!");
        }
    }
    We recommend developers to either use the chrome.storage APIs accessible only to the extension scripts and not the webpage or utilize these APIs in the background context (i.e. in the background script) which is not accessible to the Web page.
  5. PostMessage Exchanges:
    If an extension sends postMessages through their content script or even script injected into the DOM, the Web page can also listen to these messages by simply registering an addEventListener and can the infer the existence of a target extension, as follows:
    // contentScript.js or injected.js
    window.postMessage("extension-specific-data", "*")  ;
    
    // webpage-script.js
    window.addEventListener("message", function(event) {
        if (isMessageSeen(event.data)) {
            reportToServer("Target extension is installed!");
        }
    })