Most people meet this problem the same way: an upload form rejects an SVG, they assume it is an oversight, and they add SVG to the allowed types. Six months later a penetration test flags stored cross-site scripting.
This guide covers what the actual risk is, when it applies, and what to do about it. If you are downloading icons from a known source and putting them on your own site, none of this affects you. The problem is specifically about SVG you did not author being served from your origin.
The short version
SVG is XML and can contain <script>, event handler attributes and external references. If a user uploads one and you serve it from your domain, that script runs with your origin's privileges. Either sanitise properly with a maintained library, serve uploads from a separate origin, or do not accept SVG.
Why SVG is different from PNG
A PNG is a container of pixel data. A parser reads it and produces an image. There is nothing in the format that expresses behaviour, so a malicious PNG has to exploit a bug in the decoder, which is rare and gets patched.
An SVG is an XML document rendered by the browser's full rendering engine. It can contain scripts, CSS, event handlers, references to external files, and embedded content. It is closer to an HTML page than to an image, and the security model should follow from that.
This is not a flaw. It is why SVG can be animated, styled and made interactive. But it means the mental model "images are safe to accept from users" does not transfer to SVG.
What can be hidden in an SVG
An illustrative example, not an exhaustive list:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<script>/* arbitrary JavaScript */</script>
<circle cx="12" cy="12" r="10" onload="/* handler */"/>
<image href="https://elsewhere.example/pixel.png"/>
<foreignObject><!-- embedded HTML --></foreignObject>
</svg>
The categories to be aware of:
- Script elements. The obvious one, and the one naive filters catch.
- Event handler attributes.
onload,onclick,onmouseoverand many others, which can appear on almost any element. - External references. Images, stylesheets and fonts loaded from another domain. Even without script, these leak the viewer's IP address and user agent to a third party, which is a tracking vector and a data protection issue.
foreignObject. Allows arbitrary HTML inside the SVG, which widens the surface considerably.- Entity expansion. XML entities can be nested to consume enormous memory, a denial of service pattern that also affects server-side parsers.
- CSS with external imports, another route to loading remote content.
When it actually executes
This is the part that causes confusion, because the same file behaves differently depending on how it is placed.
| How the SVG is used | Script runs? | Risk |
|---|---|---|
| Inline in your HTML | Yes | High. Full access to your page |
| Navigated to directly as a URL | Yes | High. Runs on your origin |
<img src="..."> | No | Low. Scripting disabled in this context |
CSS background-image | No | Low |
<object> or <iframe> | Yes | High |
Browsers deliberately disable scripting for SVGs loaded through <img>, which is why displaying an untrusted SVG in an image tag is comparatively safe. It is not a complete defence, because the file is usually still reachable at its own URL, and a user who navigates there directly gets it as a document with scripting enabled. That is how stored XSS via avatar upload typically works.
So "we only display them in img tags" is not sufficient on its own. What matters is whether the file is reachable as a document on your origin.
Sanitising properly
Do not write your own sanitiser. Stripping <script> with a regular expression is the classic mistake, and it fails against event handlers, encoded payloads, namespace tricks and malformed markup that browsers helpfully repair.
Use a maintained library with an allowlist model, meaning it permits known-safe elements and attributes and rejects everything else, rather than trying to enumerate what is dangerous. On the server, sanitise with a library appropriate to your stack. On the client, DOMPurify is the standard choice and supports SVG explicitly.
Whatever you use, sanitise server-side on upload and store the sanitised version. Client-side sanitisation protects the current page and does nothing about the file sitting on your server, which is the thing that gets served to everyone else.
Things a good configuration removes: script elements, all event handler attributes, foreignObject, external references in href, xlink:href and style, and any element outside the SVG namespace.
The separate origin approach
Sanitisation is a filter, and filters occasionally fail. Defence in depth means also ensuring that a file which slips through cannot do much.
The standard approach is to serve user uploads from a different origin to your application: a separate domain, not a subdomain, since subdomains can share cookies. A script executing there has no access to your application's cookies, local storage or DOM. This is why large platforms serve user content from a distinct domain, and it is the single most effective structural mitigation available.
Supporting measures for the upload response:
Content-Disposition: attachmentso navigating to the file downloads it rather than rendering it.X-Content-Type-Options: nosniffto stop the browser second-guessing the content type.- A correct
Content-Typeheader, and never one derived from the uploaded filename.
Content security policy
A content security policy limits what any script can do if it does execute. It is not a substitute for sanitisation, and it is a strong second layer.
The relevant directives for SVG are script-src, which controls whether inline script can run at all, img-src, which restricts where images may load from, and object-src, which is worth setting to none in almost all applications since <object> and <embed> are rarely needed and widen the attack surface.
A policy that forbids inline script neutralises most SVG-based XSS, because the payload is inline by definition. If you cannot get there across your whole application, applying it to the origin that serves user uploads is a smaller and very worthwhile piece of work.
Icons you download yourself
To be clear about scope: none of this applies to icons you download from a known open source library and place on your own site. Those files contain shapes and nothing else, and they come from public repositories that many people read.
Two habits are still worth having. Open unfamiliar SVGs in a text editor before shipping them, particularly anything from a marketplace, a client, or a search result. You are looking for script elements, on attributes and external URLs. It takes ten seconds.
And run downloads through an optimiser. SVGO and similar tools strip editor metadata, comments and unused definitions, which reduces file size and incidentally removes a lot of what should not be there. See optimising SVG files.
Everything in the ICON OOP tool comes from public, widely audited open source projects: Lucide, Tabler, Phosphor, Material Symbols, Simple Icons, Devicon and Twemoji. The exported files contain path data and styling attributes, no script and no external references.
A checklist
- Do you accept SVG uploads from users? If not, none of this applies.
- Do you sanitise server-side with a maintained allowlist library? Not a regular expression.
- Do you store the sanitised version rather than the original?
- Are uploads served from a separate origin, a different domain rather than a subdomain?
- Is
Content-Disposition: attachmentset so direct navigation downloads rather than renders? - Is
nosniffset and the content type derived from actual inspection, not the filename? - Does your CSP forbid inline script on the origin serving uploads?
- Do you display untrusted SVG through
<img>rather than inlining it?
If you cannot answer yes to most of these, the safer decision is to convert uploaded SVGs to PNG on the server and serve the raster version. You lose scalability and you remove the entire class of problem. The SVG to PNG page covers what that trade costs you.