Corey Prophitt's Website

June 22, 2019

Nefarious LinkedIn

A look at how LinkedIn exfiltrates browser extension data from your browser.

Unraveling the Mystery

How would you feel if you opened a program and the program started to check your file system to see what other programs you had installed? You would probably feel the software was overstepping. This is essentially what LinkedIn does when you visit their website. LinkedIn will scan your local browser files in an attempt to identify a number of different browser extensions you may have installed. The data collected by LinkedIn is then exfiltrated from the browser.

This whole adventure started when I was browsing LinkedIn and happened to have my browser console open. While on my LinkedIn profile I noticed a large number of 404 errors and as a developer it piqued my interest. LinkedIn's website making local host web requests.

What really piqued my curiosity was the fact all of these failed web requests were for chrome-extension:// resources. After inspecting a few of the extension IDs in the resources I began to suspect LinkedIn was attempting to determine if I had certain extensions installed by executing local web requests to the browser itself.

I spent some time toying around with my browser and LinkedIn's assets. Note, reverse engineering LinkedIn's source code is apparently against their terms of service. If I was LinkedIn I would probably not want people figuring out how I spy on them either.

After poking around and doing some investigation I found an interesting object in one of LinkedIn's local storage values.

LinkedIn's Extension File

One of LinkedIn's local storage keys is C_C_M. The value itself is a base64 encoded string (which isn't too abnormal). However, if you decode the string you will see a large JSON blob that seems to be encoded with unicode code points (not human readable).

LinkedIn's local storage printed to a browser console encoded with unicode.

I am not too sure how or why they encoded it that way, but it seems to me like it was in an attempt to obfuscate the data. The encoding is easy enough to reverse, simply parse the JSON. You can do it yourself with the following snippet:


Doing so will display a large JavaScript object with some interesting data in it. Note, it appears the data held in this JSON blob is personalized to some degree. In other words, my JSON blob may be larger or smaller than yours. I am unsure which heuristic LinkedIn uses to determine which extensions to scan for but they must be using some. Curious what the complete JSON object looks like? Here's mine.

LinkedIn's local storage printed to the console as a javascript object.

How does LinkedIn search for extensions?

After examing the JSON file it became pretty clear what was going on. LinkedIn is using two different methods to determine if you have an extension installed.

  1. Content Changed
  2. Web Accessible Resources

The first method is the simplest. LinkedIn simply looks for certain content on the page they know doesn't belong there. For instance, if they find a div with the ID email-hunter, they know you have the Email Hunter extension installed (and now your account is probably restricted, or at least on a blacklist).

The second method is a lot more interesting to me. When building an extension you can specify web accessible resources. These resources are typically used via a content script to build a custom interface. However, there's a bit of a gotcha. If the content script can make a request for the web accessible resource, so can the underlying website. LinkedIn abuses this fact and sprays web requests to your local browser in attempt to find extensions.

I built a simple extension to automatically parse the extension file and display extensions LinkedIn is looking for. Check it out here.

What can you do as a developer to prevent detection?

So, as a developer of an extension what can you do about this? I recommend not using web accessible resources. Out of all extensions LinkedIn finds, a majority of them are due to web accessible resources.

I would recommend not modifying or injecting user interface features into the underlying page. Alternatively, I would use a browser action and communicate with the content page through messaging passing.