LinkedIn “allows members to write, edit, and distribute articles” on its platform and you can do that easily by opening this URL in your browser: https://www.linkedin.com/post/new . You can basically customize everything you want in your article and also you can embed resources like images, videos or slides. From all of these, the image upload feature caught my attention, as you will see below.

Step by step

If you want to add a new image to your article, you have the possibility to upload it by clicking on the “+” sign and then selecting which type of content you want to include in your article, in this case, an image.

Then, as you click on Upload from computer and choose an image, a new POST request will be made to the /mupld/megaImageUpload endpoint as you can see in this image:

The response of this request will be in JSON format and will contain a field called value . This will act like a unique identifier for my image and it will represent its new name once it’s uploaded on the server.

After the upload process is finished, the image will be available on https://media.licdn.com/ and in my case it was:

 https://media.licdn.com/mpr/mpr/AAMAAgDGAAgAAQAAAAAAAAtSAAAAJDBhMTg2ZWNiLWUwYzktNGMyNi05OWUxLTFlOWI0YmU0MmQzNg.jpg .

I felt that I could find something interesting here, so I started to tamper with the upload request. Firstly, I changed the extension of the image that I uploaded to .html, .htm, .xml, .svg etc. , hoping that this would make the server to deliver valid HTML content using my image. Unfortunately, this change always threw an INVALID_MEDIA error and my actions were in vain.

Then, I chose to look at the value of the Content-Type parameter that corresponded to the uploaded image and was always sent in the upload request. Initially, I tried to change this value to text/html, but, as expected, it didn’t work and I was faced with the same error message as before.

After several tests, I discovered that only the hardcoded value text/html was blacklisted. This means that I could change the Content-Type of my image to I/hackYou. Once the file is served by LinkedIn’s server, it will have my fake header in the response (Content-Type: I/hackYou).

Moreover, the server didn’t check whether the blacklisted Content-Type was written using uppercase letters and I was able to easily bypass the filter by using one of these values: text/htmL, Text/html, tExt/html,… . I didn’t know until that moment, but the browsers ignore the case of the letters when it comes to the Content-Type header and will consider “Text/Html” as an equivalent for “text/html”. As a result, every image that I uploaded, no matter if it had a valid extension, could be used to serve HTML content.

Was it necessary for the image to be valid and all the bytes from its body to be in the right order? Of course not. The backend script that was taking care of the uploaded image checked only the first few bytes of the image to ensure that it’s valid.

Now, I had a stored XSS, but my file was located at media.licdn.com, which is a sandboxed subdomain and from what I saw, it’s used by LinkedIn to host static files. I began to check every subdomain of linkedin.com to see if I could find one that had a CNAME record that pointed to that domain, but my actions weren’t successful. Also, I looked for misconfigured CORS headers and incorrect cross-domain messaging just to be sure that I did (almost) all I could.

I was ready to send this vulnerability even if it didn’t impose a high security risk, when a brilliant idea came to my mind: what if my uploaded image could be accessed through the main LinkedIn domain? I tested it, and to my surprise, it actually worked!


In order to assess this vulnerability, I thought what an attacker could do with this flaw. Well, it’s on the main domain, https://www.linkedin.com (so I didn’t have any problem with the Same Origin Policy), where I have also found the OAuth Authorization Endpoint (https://www.linkedin.com/oauth/v2/authorization). The most important thing was that this XSS bypassed any CSP restriction, just for the simple fact that the CSP header wasn’t present in the response.

So, I quickly did a PoC where I demonstrated how I could steal the anti-CSRF token from the OAuth Authorization page and use it in order to make a victim grant permissions to my application without their knowledge. You can have a look at the source code here.

Even if the LinkedIn API for developers doesn’t have so many features, I was able, for example, to create different posts on behalf of a victim and share them on their personal profiles.

In the example from above, the anti-CSRF token was stored as the value of an input tag, but, in general, LinkedIn saves it in a cookie called JSESSIONID which isn’t marked as httpOnly.

Every important action initiated by the user corresponds to a POST request that has this cookie in a header. As a result, I could, for example, get it from document.cookie using JavaScript and then send these requests using Ajax:

– I(You) could send a Private Message to your Employer

– I(You) could update your Personal Profile

– I(You) could share “articles, photos, videos or ideas” on your Personal Profile

Video PoC

See you next time.

©Alexandru Colțuneac