SECURE_NODE // ATHENS_GR
← BACK TO RESEARCH Technique · Client-side

SVG Upload to Stored XSS

20 MAY 2026| ≈ 3 MIN READ| SVGXSSCSP
SANITIZED ▸ Methodology write-up against a throwaway lab app. No client systems or live findings are described here.

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.

FINAL SEVERITY
High
stored XSS
VECTOR
SVG upload
served inline
KEY FLAW
same-origin
no isolation domain

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

T+0:00 — RECON

Found an avatar upload. Accepted images and rendered them inline in the profile header.

T+0:03 — PROBE

Uploaded a benign SVG. Confirmed it was stored and served as image/svg+xml, inline, from the app origin.

T+0:09 — EXPLOIT

Uploaded an SVG with an inline script. It executed in the app's origin when the image rendered — stored XSS confirmed.

T+0:15 — IMPACT

Demonstrated session-scoped capture. Showed the script could read the page and exfiltrate to a collector, all under the victim's session.

T+0:20 — REPORT

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: attachment for 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), and href values with JavaScript schemes before the file is accepted.
  • Treat a tight CSP as defence-in-depth, not the primary control. A strict script-src policy 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.