SVG Upload to Stored XSS
SVG files feel harmless. Most upload handlers treat them as images and move on. But an SVG is XML, and XML can carry script. That gap is where this technique lives.
Why SVG slips through
A typical upload filter checks the extension and maybe a MIME type. An SVG passes both while still being able to embed a script block or inline event handlers. When the file is later served inline in a browser context tied to the application's origin, that script runs with the victim's session.
The shape of the file
The interesting part is not the script itself — it is the delivery. An SVG is just XML that the browser will happily execute when rendered inline:
<svg xmlns="http://www.w3.org/2000/svg">
<script><![CDATA[
/* lab-only proof: attach a listener and report to a collector */
/* the point is that this runs at the app's origin, not the payload */
]]></script>
</svg>
A few conditions have to line up for it to matter: the app accepts
image/svg+xml, it serves the file inline rather than forcing a download,
and it serves it from a sensitive origin rather than an isolated sandbox domain.
Where CSP helps — and where it does not
Teams often assume Content-Security-Policy closes this. It can, but only if it
actually restricts script-src and the SVG is not served from
the same trusted origin. A permissive script-src 'self' does nothing
when the malicious SVG is served from self.
The test timeline
Found an avatar upload. Accepted images and rendered them inline in the profile header.
Uploaded a benign SVG. Confirmed it was stored and served as image/svg+xml, inline, from the app origin.
Uploaded an SVG with an inline script. It executed in the app's origin when the image rendered — stored XSS confirmed.
Demonstrated session-scoped capture. Showed the script could read the page and exfiltrate to a collector, all under the victim's session.
Documented with fixes. Isolated cookieless domain for uploads, forced Content-Disposition: attachment, server-side SVG sanitization, CSP as defence-in-depth.
Defending against it
- Serve user-uploaded files from an isolated, cookieless domain. A script executing in a separate origin cannot reach the application's authenticated session, even if it runs.
- Force
Content-Disposition: attachmentfor anything not strictly needed inline. Downloading rather than rendering a file prevents the browser from executing its content as a document. - Sanitize SVG server-side before storage. Strip all
<script>blocks, event handler attributes (onload,onerror), andhrefvalues with JavaScript schemes before the file is accepted. - Treat a tight CSP as defence-in-depth, not the primary control. A strict
script-srcpolicy limits exploitation paths but does not substitute for upload isolation and sanitization.
The takeaway
"It's just an image" is an assumption, and assumptions are exactly what we get paid to test.