August 26, 2026
How an Unsanitized SVG Upload Led to Cross-Site Scripting (XSS) and a $3,500 Bounty
When testing user profile customization settings, file upload fields are often a primary focus for security researchers. While developers…

By T4nv1
2 min read
When testing user profile customization settings, file upload fields are often a primary focus for security researchers. While developers typically restrict file extensions to common image formats like JPEG or PNG, SVG (Scalable Vector Graphics) files are frequently overlooked. Because SVG is an XML-based image format, browsers parse and execute embedded markup — including inline JavaScript — if the file is rendered directly in the user's browser context.
While reviewing a corporate collaboration platform, the user settings interface allowed employees to upload custom company logos for internal team workspaces. Examining how the application validated and served vector graphics revealed a Stored Cross-Site Scripting (XSS) vulnerability, resulting in a $3,500 bounty award.
Phase 1: Analyzing the Upload Validation
The application permitted workspace managers to upload a custom logo via the organization settings page. The upload endpoint accepted several image formats, including .svg files.
The outgoing HTTP POST request was captured during a standard file upload:
HTTP
POST /api/v1/workspace/logo HTTP/1.1
Host: app.enterprise-collab.com
Authorization: Bearer eyJhbGci...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX712
------WebKitFormBoundaryX712
Content-Disposition: form-data; name="file"; filename="company_logo.svg"
Content-Type: image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
------WebKitFormBoundaryX712--POST /api/v1/workspace/logo HTTP/1.1
Host: app.enterprise-collab.com
Authorization: Bearer eyJhbGci...
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryX712
------WebKitFormBoundaryX712
Content-Disposition: form-data; name="file"; filename="company_logo.svg"
Content-Type: image/svg+xml
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
<circle cx="50" cy="50" r="40" fill="blue" />
</svg>
------WebKitFormBoundaryX712--The server accepted the file and responded with a direct URL path pointing to the stored asset:
JSON
{
"status": "success",
"file_url": "https://app.enterprise-collab.com/uploads/logos/logo_99812.svg"
}{
"status": "success",
"file_url": "https://app.enterprise-collab.com/uploads/logos/logo_99812.svg"
}Phase 2: Testing XML Content Handling
To evaluate whether the application sanitized XML markup or stripped script tags upon upload, a test SVG file containing a basic script block was constructed.
The crafted file was uploaded via the same endpoint. The server accepted the file without throwing a validation error or stripping the internal <script> tags.
The Flaw: Same-Origin Asset Delivery
Accepting SVG uploads is not inherently vulnerable if the server enforces safe delivery controls:
- Serving uploaded assets from an isolated, sandboxed domain (e.g.,
user-content.cominstead ofapp.enterprise-collab.com). - Setting the
Content-Disposition: attachmentheader to force the browser to download the file rather than render it inline. - Overriding the MIME type to
text/plainor enforcing a strictContent-Security-Policy(CSP).
Inspecting the HTTP response when accessing the uploaded file directly revealed missing defensive headers:
HTTP
HTTP/1.1 200 OK
Host: app.enterprise-collab.com
Content-Type: image/svg+xml
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400
<svg ...><script>alert(document.domain);</script>...</svg>HTTP/1.1 200 OK
Host: app.enterprise-collab.com
Content-Type: image/svg+xml
Access-Control-Allow-Origin: *
Cache-Control: public, max-age=86400
<svg ...><script>alert(document.domain);</script>...</svg>Because the SVG file was served directly from the main application domain (app.enterprise-collab.com) with a Content-Type of image/svg+xml and without an enforcing CSP, navigating to the file URL caused the browser to execute the embedded JavaScript in the context of the main application domain.
[ Attacker Uploads Malicious SVG ] ──> [ Main Application Domain ]
│
▼
[ Stored at /uploads/logos/logo.svg ] ──> [ Serves File inline as image/svg+xml ]
│
▼
[ Victim Visits Logo URL ] ────────────> [ Script Executes in App Context ]
(Accesses Session Cookies/DOM)[ Attacker Uploads Malicious SVG ] ──> [ Main Application Domain ]
│
▼
[ Stored at /uploads/logos/logo.svg ] ──> [ Serves File inline as image/svg+xml ]
│
▼
[ Victim Visits Logo URL ] ────────────> [ Script Executes in App Context ]
(Accesses Session Cookies/DOM)If an administrator or team member opened the uploaded logo link, the embedded script executed with full access to the user's active session state and DOM environment.
Triage & Resolution
The vulnerability was documented with a minimal proof-of-concept payload showing execution context via document.domain.
- Vulnerability Class: Stored Cross-Site Scripting (XSS) via SVG Upload
- Severity Rating: Medium / High
- Time to Triage: 3 Hours
- Final Award: $3,500 Bounty
The engineering team implemented the following fixes:
- Re-routed all user-uploaded static assets to serve from an isolated, cookie-less domain (
enterprise-collab-usercontent.com). - Implemented strict XML sanitization on uploaded SVG files to strip JavaScript event handlers and
<script>elements prior to storage. - Added the
Content-Security-Policy: default-src 'none'header on raw asset delivery endpoints to prevent script execution on static files.
Critical Lessons for Bug Hunters
- Verify Asset Delivery Domains: Always check if uploaded files are served from the main application domain or an isolated CDN/user-content domain.
- Inspect Content-Type and Disposition Headers: Check whether the server forces inline rendering (
image/svg+xml) or forces file downloads (Content-Disposition: attachment). - Test Event Handlers: Aside from standard
<script>tags, test inline event handlers inside SVG elements, such as<svg onload="...">or<image href="..." onerror="...">.