Working on new methods and tools to identify browser exploits, I recently came across a common question again in a forum: "Is it possible to detect what browser extensions I have installed?"
That information would be of value to various people for several reasons. Online attackers and snoops stand to gain most from it. Examples:
Besides the usual suspects, who else would benefit from knowing which browser extensions are installed on a given client? Take online advertising firms, for example. Addon insights can help them build target profiles based on user interests and preferences.
Online advertisers and malicious actors alike need as much data from the client as possible, and the local browser stands at attention to do their bidding. Detecting extensions is only one of many ways to distinguish one machine's environment from another.
Ideally, traditional web browsers and their extensions should be built with your security and privacy in mind and shouldn't be detected by any external service. Since that is not the case, we better face the harsh reality together and ask: How much - or little - does it take to query what extensions are installed on a browser and get a list of them? Let's dive in.
Google Chrome and Mozilla Firefox are the two browsers we're going to run tests against. They have the most extensive collection of various browser plugins you can install from their respective web stores.
The way Google Chrome handles extension queries is through its chrome-extension:// URI scheme. This URI operator handles everything related to a browser extension. It works the same way with all extensions present on Chrome:
chrome-extension://<UNIQUE EXTENSION ID>/<RESOURCE BEING REQUESTED>
Just like any ordinary URL operator - file:///, https://, http://, et al. - we can load its resources directly in the browser.
But before we do any of that, we need the extension ID for the extension we're trying to detect. Here's how we obtain it:
We can look up any extension on Chrome in the Google Chrome web store and find the extension's unique ID in the URL, as in the screenshot above. This is the same ID we're going to use with the chrome-extension:// operator.
Now that we obtained the ID, we need a file/web-resource to request that sits inside the extension to see if it's available to interact with. I found out that every Chrome extension has a file called manifest.json within its root directory. This file contains information like the version of the installed Chrome extension, some file paths, and more.
Here's how the manifest JSON file looks like when you render it inside the browser. This example is taken from a popular Chrome extension, Google Translate. The request to render this page looks like this:
If you have Diffeo installed, and you load that URL from within your Chrome browser, it loads the manifest.json file along with all the attributes defined within it. This includes the extension's version.
Now the question becomes: If we were to collect enough Chrome extension unique IDs, going by plugin popularity, would we be able to request the manifest.json of each installed extension and find out, at the bare minimum, what version it is?
I will then install all extensions and see if I can programmatically detect each one after flipping them on.
Well, it works - kind of. Here's what it looked like from my console:
All extensions denied the request except one. From the one extension that didn't deny my request for manifest.json, I was able to retrieve and query the entire JSON object stored.
Note the many variables defined from within. They include the extension description, the version number, and different operators to use, such as the blob URI scheme. Interestingly enough also included: the web resources (*.html, *.js, *.css, et al.) used by the extension itself.
So why did all the other extensions deny my client-side request? It turns out, the folks at Google know about this method used for tracking, and they have limited the scope in which clients can communicate with extensions.
In the extension that allowed the request, what stands out is the content of the JSON variable "web_accessible_resources". Let's see how it differs:
Web-accessible resources are in charge of limiting which file resources the client is allowed to access from within the browser. This extension happened to define this interaction by using "/*" as one of the variables.
The problem with this particular definition is that the wildcard "/*" includes every resource within the extension, including manifest.json (which is located at /manifest.json). So you can see why it responded to my query.
We now know that thanks to Google's foresight, we can't just request any web resource from an extension. What we can do is request whatever is available to us.
Once we go into each extension and visit manifest.json, we can read the web-accessible resources attribute. That way, we can find web resources and request them to see if they're available.
Here I've modified the request sent to each extension. It contains a web resource accessible by the client. If the web resource allows manifest, then I use fetch to retrieve it for processing and finding out what the version number is. If it doesn't allow manifest, then I request something else that it does allow.
When making the request, if the status returned is 200, then the extension is installed, and it exists within the browser environment. If it returns a 404, then the extension is either off or does not exist.
What if it returns neither? In that case, Chrome is messing with our request, possibly leaving it in a continuous state in which we cannot determine whether or not the user has installed it.
As you can see, it works like a charm. My script was able to detect all my installed extensions by side-loading available resources. Since everything seemed to be working as intended, I exported the script to an external server.
When I tested the same script on an external server, I got mixed test results. Per extension that was turned on, I would receive a blank result (as if the array index didn't exist). Per the extension that was turned off, I would receive an error (neither status 404 or 200). This was odd, given that when I loaded the script locally, it worked perfectly fine.
Then I realized what could be happening; it is possible that Chrome handles specific requests differently based on if they're loaded using file:/// versus https:// or http://. This could pose an issue.
Still, if I find that errors are handled in a consistent manner when the client makes an external request from a script loaded remotely, I should be able to determine if the extension exists.
This approach would require processing errors as "extension not existing" and blank responses as "extension existing" using try-and-catch methods, and then transforming the findings to their expected result (received error == "extension does not exist", no error == "extension does exist").
For Chrome in particular, Google supports direct calls from/to extensions - as long as the extension ID is present. Google outlines how the API "includes support for exchanging messages between an extension and its content scripts or between extensions." How to make such requests is explained here.
var myPort=chrome.extension.connect('Extension_ID', Object_to_send);
So what's the takeaway? Extensions can be powerful tools, yet many are developed without giving much thought to security and privacy. In some cases, extensions allow access to their resources in any context, and this can pose a risk to individual users and, through them, to the whole network.
This post outlines only one method of detecting extensions. There are other methods for achieving the same results.
Maybe you are fed up with ad networks and marketers that are violating your privacy, using the browser and its extensions as their gateway to your data?
Or perhaps you're in IT and worried about local browsers and their extensions putting your organization at constant risk of attacks and de-anonymization?
Then you want to minimize attribution based on extension fingerprinting. This measure is especially vital for users conducting sensitive OSINT research and investigations online.
On the web, appearing unique compared to all in the eyes of adversaries who know where to look, puts a bull's eye on your back. To prevent attribution and de-anonymization, beware - and drop - those chatty browser extensions.
And if such considerations are essential in your line of work (as a cybersecurity threat hunter, for example, or as a fraud investigator) - consider switching to a secure OSINT research platform with managed attribution altogether.