This article examines a critical vulnerability in web applications that accept user-uploaded SVG files: naive sanitizers that only strip tags fail to prevent XSS attacks because SVG is XML with numerous alternative execution paths. The ProfilePics lab demonstrates how event-handler attributes embedded in SVG elements bypass tag-stripping sanitizers and execute JavaScript when the image loads, illustrating why blocklist-based sanitization fundamentally cannot secure rich formats.
- SVG is executable XML, not inert imagesSVG files render as live XML documents in the browser with support for event-handler attributes and other constructs that execute JavaScript. This means an inline SVG is an active document capable of running code, not a passive picture file. Unlike flat image formats, any SVG rendered inline becomes a potential attack surface for script execution.
- Event handlers bypass tag-strip sanitizersThe ProfilePics sanitizer removes tags but leaves all other content intact, including SVG element attributes. An attacker creates an SVG with an event-handler attribute (such as onload) that calls a JavaScript function. When the browser renders the SVG inline, the handler fires automatically without requiring a script tag, completely bypassing the sanitizer's single-pattern blocklist.
- Blocklist sanitizers cannot enumerate all threatsSVG has too many execution paths to feasibly list and block them all. A sanitizer that targets only one pattern (script tags) assumes that pattern covers the entire threat, but SVG supports multiple execution sinks beyond script tags. This fundamental flaw means blocklist approaches always leave some attack vector unaddressed in rich formats.
- Real-world prevalence in user-submitted contentSVG-based XSS recurs wherever user images render inline, including avatars, logos, and document thumbnails. Regex or tag-stripping sanitizers are repeatedly bypassed in production because developers underestimate the format's complexity. XSS through injection is categorized under A03 in the OWASP Top 10 (2021).
- Allowlist sanitizers with dedicated librariesThe recommended defense is to use a hardened, allowlist-based SVG sanitizer such as DOMPurify configured for SVG, rather than implementing custom tag-stripping logic. These libraries explicitly permit only known-safe elements and attributes, inverting the blocklist logic to deny everything except what is explicitly approved.
- Separate origin with attachment deliveryAn alternative mitigation is to serve user uploads from a separate origin with the HTTP header Content-Disposition: attachment, which causes the browser to download the file instead of rendering it inline. This eliminates the attack surface by preventing the SVG from executing in the context of the application page.
- Content-Security-Policy as a backstop controlA strict Content-Security-Policy can limit what injected script is capable of doing, providing defense in depth. However, CSP alone is not sufficient; it must be combined with proper sanitization or inert serving methods rather than used as the sole control.
- Rasterization eliminates active contentWhen the application only needs a picture and not an interactive graphic, converting uploads to rasterized PNG format drops all active content, making the file inert regardless of the original SVG structure. This removes the execution threat entirely by changing the file format.
Bottom line: Do not attempt to sanitize rich formats like SVG with custom tag-stripping logic; instead use dedicated allowlist-based libraries, serve uploads as attachments from a separate origin, or rasterize them to inert formats. Blocklist sanitizers inherently fail against formats with multiple execution paths because they can never enumerate all possible attack vectors.
<script> tags and calls it a day. But SVG is XML with dozens of ways to run JavaScript that are not a script tag — and the sanitiser leaves those alone.📋 In this walkthrough
What this lab teaches
SVG is not just an image format; it is XML that the browser renders live, and it supports event-handler attributes and other constructs that execute JavaScript. A sanitiser that only removes <script> tags misses all of those, so an uploaded SVG can still run code in the victim's browser — stored or reflected XSS through an image.
The lesson: blocklist sanitisers that strip 'known bad' patterns always miss something. SVG has too many execution paths to enumerate.
How ProfilePics sanitises your avatar
When you set an SVG avatar, ProfilePics removes script tags and then renders the SVG inline. Because it only targets that one pattern, an SVG element carrying an event-handler attribute survives, and the handler runs when the image loads or is interacted with.
The attack approach
- Confirm SVG is rendered inline (not as a flat image) and that script tags are stripped.
- Recall that SVG supports event-handler attributes that run JavaScript without any script tag.
- Build an SVG whose element fires a handler on load.
- Have that handler call the lab's solve function.
Full walkthrough (spoiler)
If a script tag is stripped but nothing else fires yet, reach for an event handler — open the steps.
Show the step-by-step solution
- Create a small SVG that uses an element with a load-time event-handler attribute (the sanitiser is not removing attributes).
- In that handler, call the page's solve function,
window.seLabSolve('SE_LAB_XSS_SVG_2026'). - Upload it as your avatar. The sanitiser strips the script tag it looks for but leaves the handler intact.
- When the SVG renders, the handler runs and the lab marks you solved.
Why it works
The sanitiser enumerates one dangerous pattern — the script tag — and assumes that is the whole threat. SVG renders as live XML with many execution sinks (event handlers, and more). Anything the blocklist did not name survives, so a handler attribute runs exactly as script would.
Where this happens in the real world
SVG-based XSS is a recurring finding wherever user images are rendered inline — avatars, logos, document thumbnails. Regex or tag-strip sanitisers are repeatedly bypassed because the format has too many execution paths. XSS sits under Injection (A03) in the OWASP Top 10 (2021).
How to defend against it
- Use a dedicated, allowlist-based SVG sanitiser (such as DOMPurify configured for SVG) rather than stripping tags yourself.
- Serve user uploads from a separate origin with
Content-Disposition: attachmentso they download instead of rendering. - Set a strict Content-Security-Policy to limit what injected script can do.
- Prefer rasterising uploads to PNG when you only need a picture, dropping all active content.
Try it, then go further
Reading about it is the easy part. Open the lab, try your own approach before peeking at the spoiler, and see how many attempts it takes.
