Vectorr

SVG Security: Why the Format Can Carry Scripts

An SVG is not an image in the way a JPEG is an image. It is an XML document that browsers will happily execute, fetch and evaluate. That makes it a useful file type and a hostile one to accept blindly.

November 6, 2026 · 8 min read

Upload an SVG to a web application and you may have handed the server a document that can run script, fetch external URLs, expand an entity bomb, and phone home to a tracking pixel. Upload a JPEG and the worst case is a decoder bug. The difference is structural: SVG is executable markup, not a bitmap.

This matters most in three places — a browser rendering an untrusted SVG, a server-side upload or conversion pipeline, and a CDN serving user files from the same origin as the application.

What an SVG can contain beyond shapes

The XML root can hold child elements that are not drawing instructions at all. The SVG specification allows a <script> element, and it allows event attributes such as onload, onclick, and dozens of others on almost any element. These are the same JavaScript event attributes HTML has.

That is intended behaviour. Interactive SVG documents — diagrams with clickable regions, animated explainers, charts with tooltips — depend on it. But it means opening an SVG in a browser is closer to opening an HTML page than to opening a PNG.

Most importantly, when an SVG is loaded as a top-level document rather than through an <img> tag, any script it contains runs in the origin that served it. If your application serves user-uploaded SVGs from its own domain, script inside those files runs with your domain's privileges and can read cookies, call your APIs, and read local storage.

External references

An <image> element can point at a remote URL. So can a <use> element, a filter reference, a gradient reference, and CSS url() values inside a <style> block. Every one of those triggers an outbound HTTP request when the document is rendered.

Two attacks live here. The first is a tracking beacon: an image reference to an attacker-controlled host with a unique identifier in the query string tells the attacker when and where the file was opened. The second is server-side request forgery: if your backend rasterises SVGs, the rasteriser may follow those URLs from inside your network, reaching internal metadata endpoints or admin interfaces that are not publicly reachable.

VectorWhere it bitesImpactMitigation
<script> elementBrowser, same-origin servingStored XSS; session theftStrip in sanitiser; serve from a separate origin
on* event attributesBrowserXSS on interaction or loadAllowlist attributes; never pass through unchanged
javascript: hrefBrowserExecution on clickReject non-safe URL schemes
External <image href>Browser, server rasteriserTracking, SSRF, IP leakInline or block remote references
XXE entity definitionsServer-side XML parserFile disclosure, SSRFDisable external entities and DTDs
Billion laughs entity expansionServer-side parserMemory exhaustion, DoSLimit entity expansion; cap input size
Deeply nested groupsRenderer, rasteriserCPU/memory DoSLimit nesting depth and node count
Huge filter regionsRasteriserMemory exhaustionStrip filters or cap output pixels
Oversized viewBoxRasteriserMassive raster allocationClamp target dimensions server-side

What matters for a browser

A browser treats a top-level SVG as a document. Script runs, external requests fire, and same-origin policy applies to the serving origin. If you serve user SVGs from https://yourapp.com, the script inside them runs as yourapp.com.

Three defences reduce this to near zero, and they are independent:

  1. Serve user-uploaded content from a separate origin or a cookie-less subdomain, so script cannot reach your session cookies or call your authenticated APIs.
  2. Send Content-Disposition: attachment for direct downloads, so a navigation to the file downloads rather than renders it.
  3. Send a strong Content Security Policy with script-src 'none' on the response that serves the file.

The <img> element has different rules. When an SVG loads through <img src=\"file.svg\">, browsers disable scripting and do not run external resource loads the same way. That is why referencing untrusted SVGs via <img> is much safer than inlining them into the page or navigating to them. Inline SVG — pasting markup into the HTML — is the most dangerous pattern, because the markup lands in the page's own DOM.

What browsers already protect against

Modern browsers disable external entity resolution in XML, so classic XXE against a browser is largely historical. They also apply origin boundaries to SVG served cross-origin. These protections do not extend to your server-side tooling, which is where the remaining risk concentrates.

What matters for a server-side pipeline

If your application accepts an SVG upload and then parses, sanitises, measures, or rasterises it, the XML parser itself becomes an attack surface. The classic vulnerabilities are well documented and still reproducible in libraries configured with defaults.

XXE. A DTD declares an entity that points at a local file or a URL. When the parser expands it and the content is echoed or embedded in output, the file is exfiltrated. Disabling DTD processing entirely is the clean fix; there is no legitimate reason for an SVG from an untrusted source to define custom entities.

Entity expansion. The billion laughs pattern nests entity references so that a tiny document expands into gigabytes. Limits on total expansion, or rejection of any internal DTD, close it.

Rasteriser resource exhaustion. Converting a vector to a bitmap is proportional to target pixel count, not input file size. A one-kilobyte SVG with a viewBox the size of a billboard and a request to render at 300 DPI can ask for several gigabytes of memory. Cap the target dimensions on the server and reject anything beyond a sane maximum.

Filters and nested groups. A small document with deeply nested groups each carrying a blur filter causes multiplicative work. Depth limits and a filter allowlist are the defence.

The sanitisation approach that works

Use an allowlist, not a blocklist. Blocklists lose because the SVG specification and browser behaviour keep adding attributes, and because parsers accept spelling variants and namespace tricks that a naive string match misses.

A working allowlist for image-oriented SVGs:

  • Elements: svg, g, path, rect, circle, ellipse, line, polyline, polygon, defs, linearGradient, radialGradient, stop, use, title, desc.
  • Excluded entirely: script, foreignObject, style, iframe, audio, video, animate variants that set href.
  • Attributes: geometry and paint only — d, x, y, width, height, rx, ry, cx, cy, r, points, fill, fill-opacity, stroke, stroke-width, stroke-linecap, stroke-linejoin, opacity, transform, viewBox, preserveAspectRatio.
  • Always rejected: any attribute beginning on, any href or xlink:href that is not a same-document #id fragment, and any value beginning javascript: or data:text/html.

foreignObject deserves special mention. It allows arbitrary HTML — including script — inside an SVG. Almost no legitimate logo file needs it, and it defeats most naive filters because the payload is not in an SVG element at all.

Content sniffing and MIME types

Browsers decide how to render a file from its Content-Type header, with some sniffing. If your server labels a user-uploaded SVG as text/plain, most browsers will not execute it and the risk drops. If it serves it as image/svg+xml, the file becomes an active document. That header is a security control, not a formality.

Serving with Content-Type: text/plain plus Content-Disposition: attachment and X-Content-Type-Options: nosniff produces a file that a browser downloads rather than renders. Users who want to preview it see a stored raster instead. This combination closes the browser-side attack surface for user uploads more completely than any sanitiser written in-house.

Sanitiser libraries and their limits

Maintained libraries exist for the purpose and generally do the job well, provided they are configured with a strict policy rather than a permissive one. The pattern in practice is to run the library with the same conservative profile you would use for HTML, then to additionally block foreignObject, which most HTML sanitisers permit because HTML needs it and SVG does not.

The failure mode to watch for is a sanitiser applied to a parsed DOM and then the original file written back. If the clean output is not what gets stored, the sanitiser did nothing but use CPU. Verify by re-parsing what was persisted and checking that the document contains only allowlisted elements.

Chain of custody for converted files

Conversion is a trust boundary. An untrusted SVG that has been converted to PNG is no longer dangerous, but the intermediate file and the conversion process are. Keep the original quarantined, never let it reach a path that will eventually be served, and treat the rasterised output as a new artifact with its own integrity checks.

If you offer conversion as a product feature — taking customer artwork and returning a cleaned vector, for instance — the safest architecture is to convert into a raster, display that for approval, and only generate the vector output from geometry your own pipeline produced. That way no untrusted markup ever leaves your infrastructure intact.

Detecting malicious intent versus bad tooling

Not every hostile-looking SVG is hostile. Design tools routinely emit empty style blocks, namespace declarations, comments containing the author's username, and references to local font files. A file full of what looks like noise is usually just Illustrator being Illustrator.

Distinguish by behaviour. A script element with an actual payload, an onload attribute defining a function, or an href pointing at an external domain with a query string — those are intentional. A style block with a reset rule is not. Log what your sanitiser removed so you can review the difference in aggregate rather than inspecting every upload by hand.

Safe rendering of untrusted files

When you must display a user's SVG and cannot guarantee it has been sanitised, rasterise it server-side. Rendering the image to a PNG means the output is pixels, and pixels cannot execute. This is the approach large platforms use for avatar and logo uploads. Do the rasterisation in an isolated process with memory and CPU limits, with no network access.

The same principle makes SVG to PNG a useful security control, not just a format conversion. If your pipeline produces a PNG preview, serve that preview and never serve the original file from your main origin.

If the rasteriser runs in a sandbox with no egress, the SSRF class disappears entirely. If it also has a hard memory cap, the resource exhaustion class disappears. Both are configuration, not code.

A practical hardening checklist

  1. Parse SVG with an XML library configured to forbid DTDs and external entities.
  2. Cap input file size before parsing.
  3. Run the sanitiser against an element and attribute allowlist.
  4. Reject or strip all on* attributes and non-fragment hrefs.
  5. Serve user content from a distinct origin, with Content-Disposition: attachment on download routes.
  6. Do all rasterisation in a network-isolated sandbox with memory limits.
  7. Store the sanitised copy, and treat the original as untrusted forever.

If you accept customer artwork as a matter of course, handling bulk customer artwork covers the intake side of the same problem, and what print shops ask customers shows the file requirements that reduce surprise uploads.

For background on what the format legally contains, see what an SVG actually is. If your concern is cleanup for machinery rather than security, the SVG optimisation guide covers stripping extraneous markup, and batch conversion workflows covers running the same treatment across a queue of files.

Frequently asked questions

Can an SVG file contain malware?

It can contain script, event handlers and external references, because SVG is XML markup that browsers execute. A well-known SVG can carry cross-site scripting payloads that run when served from your domain. It cannot infect an operating system the way a binary executable does, but it can steal sessions and pivot through your application.

Is loading an SVG in an img tag safe?

Much safer than inlining it. When an SVG loads through an img element, browsers disable scripting and block most external resource loads. The danger returns if you inline the markup into your page's DOM, embed it in an iframe, or navigate directly to it on your own origin.

What is XXE and does it apply to SVG?

XXE is XML External Entity injection: a DTD defines an entity pointing at a local file or URL, and the parser expands it into the document. SVG is XML, so it applies wherever an SVG is parsed by a server-side library that has not disabled external entities. The fix is to disallow DTD processing entirely.

How do I sanitise an SVG before serving it?

Parse it with an XML parser that rejects DTDs, then rebuild the document using an allowlist of safe elements and attributes. Drop script, foreignObject, style and all on* event attributes. Reject any href that is not a same-document fragment. Then serve the rebuilt file from a separate origin.

Why rasterise untrusted SVGs instead of cleaning them?

Rasterisation reduces the output to pixels, which cannot execute or make network requests. It closes every markup-based attack class at once, including ones you did not anticipate. The cost is that the result is no longer vector, so keep the original quarantined and regenerate if needed.

Sources

Convert files for this workflow

Try it on your own file

Drop in an image or an SVG and see the result before you pay. No signup is required to preview.