Is SVG safe? Scripts, sanitizing and uploads
When an SVG can run code, why the same file is safe in an img tag but risky inline or at its own URL, and how to handle uploads and token logos.
5 min read
Short answer: an SVG from a source you trust, shown with an <img> tag, is as safe as a PNG. The risk appears in two situations: when you accept SVG files from other people and serve them back, and when you insert SVG markup you did not write directly into your page. Crypto products hit both, because users upload avatars and project logos, and wallets display token logos from hosts they do not control.
Why an image format can run code
SVG is an XML document format, not just a picture format. Besides shapes, the specification allows elements and attributes that do things:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" onload="alert(document.domain)">
<script>alert('script element')</script>
<a href="javascript:alert('link')">
<circle cx="12" cy="12" r="10"/>
</a>
</svg>
That small file carries three ways to run JavaScript: a <script> element, an event handler attribute (onload, and others such as onclick or onmouseover), and a javascript: link. SVG can also embed HTML through <foreignObject> and point to external files through href attributes and CSS.
None of that is unusual or malicious in itself. Interactive diagrams use it legitimately. The question is whether the browser will execute it, and that depends entirely on how the file is loaded.
Same file, different behavior
| How the SVG is loaded | Scripts and handlers | External resources | Risk with untrusted files |
|---|---|---|---|
<img src="logo.svg"> |
Never run | Not loaded | Low |
CSS background-image or mask-image |
Never run | Not loaded | Low |
Inline markup (innerHTML, dangerouslySetInnerHTML, a template) |
Can run, as part of your page | Loaded | High |
Opened directly at its URL, or in <iframe>, <object>, <embed> |
Run, on the origin that served it | Loaded | High |
Browsers treat SVG used as an image as a static picture: no scripts, no interaction, no network requests from inside the file. That is why <img> is the default recommendation for logos you did not draw yourself. The other two rows are where problems come from.
Inline SVG becomes part of your document. Any event handler or javascript: link in it runs with your page's privileges, which means it can read what your page can read and make requests as the signed-in user. A file opened directly at its URL is a full document in its own right. If it is served from your app's domain, its scripts run on your origin.
The upload problem
Picture a dashboard where projects upload their token logo. You store it at https://app.example/uploads/logo.svg and show it with <img>. Everything looks fine, because in an image tag nothing executes.
Then someone shares the direct link to that file. Anyone who opens it gets an SVG document served from your app's own origin, and any script inside it runs there. That is a stored cross-site scripting bug, and it bypasses the care you took with the <img> tag.
Defend in layers, so that no single mistake is enough:
1. Avoid SVG uploads if you can. Accept PNG or WebP for avatars and logos. If you need vector input, rasterize uploaded SVGs to PNG on the server and only ever serve the PNG. A raster image cannot carry scripts.
2. Sanitize with an allowlist. If you must keep SVG, run it through a maintained, allowlist-based sanitizer that keeps known-safe elements and attributes and drops everything else. DOMPurify, a widely used sanitizer, has an SVG profile:
import DOMPurify from 'dompurify'
const clean = DOMPurify.sanitize(uploadedSvgText, {
USE_PROFILES: { svg: true, svgFilters: true },
})
It runs in the browser, or on a server with a DOM implementation. Do not write your own regular expressions to strip <script>: there are too many alternative spellings, encodings and attributes to catch. Also note that an SVG optimizer is not a sanitizer. Its job is making files smaller, not making them safe.
3. Serve uploads from a separate origin. Put user files on a domain that holds no cookies and runs none of your app, such as a dedicated user-content domain. If a script slips through, it runs somewhere with nothing to steal.
4. Send the right headers on every uploaded SVG:
Content-Type: image/svg+xml
X-Content-Type-Options: nosniff
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; sandbox
The Content-Type must be image/svg+xml; browsers will not render SVG in an <img> with a different type, and serving it as text/html would make things worse. nosniff stops the browser from guessing a different type. The Content Security Policy applies when someone opens the file directly: it blocks scripts and external loads and sandboxes the document, while still allowing the inline styles many SVGs use. Add Content-Disposition: attachment if the file should download rather than open.
5. Limit size and complexity. Cap upload size and reject files with an unreasonable number of elements. A valid SVG with millions of path points can freeze a tab without containing any script.
Inline SVG from your own code
Inlining is safe when the markup is part of your source code or comes from your own build step. Your icon components are no more dangerous than the rest of your JSX.
It is not safe to fetch an SVG at runtime from a URL you do not control and inject it with innerHTML so you can recolor it. If you need to tint a remote single-color icon, use it as a CSS mask instead. The image stays a static image, and you still get to choose its color.
A strict Content Security Policy on your app pages is a useful backstop. A policy without 'unsafe-inline' in script-src blocks inline event handlers, so a handler that slips into inlined markup will not run. See inline SVG vs img vs CSS background for the full trade-offs between the three methods.
Token logos from third parties
Wallets, explorers and swap interfaces often show logos from token lists, where each entry's logoURI points to some host on the internet. Treat those images as untrusted input:
- Render with
<img>, never inline. This alone removes script execution from the picture. - Consider proxying. An image proxy that fetches the logo, re-encodes it to PNG or WebP at fixed sizes, and serves it from your CDN removes any embedded code, keeps logos available when the original host goes down, and stops the original host from seeing your users' IP addresses on every page view.
- Restrict where images can come from. If everything goes through your proxy, a CSP
img-srcdirective limited to your own domains blocks stray external images. - Set sizes and a fallback. Give every logo explicit
widthandheight, and show a neutral fallback when the image fails to load.
How token lists deliver logos, and how to cache and validate them, is covered in token lists and logoURI.
One last point is about trust, not code. A safe image is not a trustworthy one. Anyone can copy a well-known logo onto a token they created. Never let a logo stand in for verification of a contract address; fake tokens and spoofed logos covers designing around that.