Cache Poisoning Leads to Stored XSS: Manipulation of Response via Accept Header

Senior Student of Computer Science | 21 y/o Web Application Pentester My HackerOne Profile: https://hackerone.com/amir_shah
Introduction
Hi everyone,
This is my very first write-up in my bug bounty journey, which began in January 2023.
Let me introduce myself: I’m Ali Hussainzada, a 21-year-old senior Computer Science student and part-time bug hunter, known online by the pseudonym Amir Shah.
This write-up details an XSS vulnerability I discovered in collaboration with my friend Saboor Hakimie. He recently earned his CPTS certification from HTB—congratulations to him!
The Bug
Without further ado, let’s dive into the vulnerability.
I was invited to a private program on HackerOne, which we’ll refer to as company.com. After discovering a one-click account takeover and some informative bugs :), I decided to explore subdomains. I generally start with the main apps, then I focuse on subdomains.
I identified a subdomain, sub.company.com, where the response code was not 200 OK. Despite this, I performed fuzzing to find a directory. I discovered a path that returned our User-Agent and IP address (/home/info). By proxying the request through Burp Suite and modifying my User-Agent, I observed that the changed User-Agent was reflected in the response. However, I encountered two issues:
The response was text/plain, meaning that if we injected an XSS payload, it would not execute.
The injected text was self-reflected, which limited its impact. we had to find a way to deliver it to victims.
Noting that the subdomain was behind a CDN, I considered the possibility of cache poisoning. I modified the endpoint to a cacheable request by adding a static extension like .js, resulting in a request such as sub.company.com/home/info/something.js This solved the first challenge, but I still needed to escalate to something more severe, like XSS.
Here’s where my friend stepped in. During a call, he asked about the HTTP request header needed to alter the response. Without waiting for my response (I did not know though), he changed the Accept header to text/html (i.e., Accept: text/html). To our surprise, the response was transformed into HTML, enabled us to inject our payload and get XSS.

Below, I’ve attached a screenshot of the original report with step-by-step reproduction details, so you can thoroughly understand the bug.

Bounty
The bug has been fixed, and I received a $1,250 reward. We split the bounty, but the company has not yet paid Saboor his portion. The original payout is $2,500, and we've been waiting since March.
Takeaway
The key takeaway here is that we can sometimes change the response (a tip I recently came across in the API Hacking book). At that time, this was new to me, but I hope you all find it helpful.
Best
I hope you enjoyed and learned something new from this article I look forward to seeing your happiness and success, so please send your positive vibes my way. :)
Thanks for reading, sharing and everything that I don't know.
What’s next? Who knows? But, i decided to write more about my findings :)
Oh, I found a similar issue on a subdomain where I can inject my payload into the User-Agent, but it’s self-reflected and i was not able to cache it, If anyone has any ideas or wants to collaborate, feel free to DM me on Twitter
My Twitter




