# The One Image WordPress Refuses to Accept: A WordPress SVG Upload Guide

Every WordPress site eventually hits this. A designer hands over a logo as an SVG, you drag it into the Media Library, and WordPress refuses it with *Sorry, you are not allowed to upload this file type.* The file is a few kilobytes of plain text. Your site accepts 40MB videos without complaint. And yet the logo — the single most important image on the site — is the one thing core won't take.

So you search for it, and the internet hands you the same four-line snippet it's been handing people for a decade. Paste it into `functions.php`, SVG uploads start working, everyone moves on. What almost nobody explains is what that snippet actually switched off, or why WordPress had an opinion in the first place.

The last SVG cleanup we did was on a marketing site where exactly that had happened. Someone had pasted the snippet a year earlier, the brand icon set went into the Media Library as SVGs, and one of those files carried an `onload` attribute that had no business being in a logo. It hadn't fired on a single page view. It was sitting there waiting for a context that would actually run it, which is a narrower set of circumstances than most SVG scare posts describe, and also the part they consistently get wrong.

---

## Why WordPress Blocks SVG in the First Place

WordPress ships an allowlist, not a blocklist. The function `wp_get_mime_types()` in `wp-includes/functions.php` holds every extension core is willing to accept, mapped to its MIME type. If your extension isn't a key in that array, the upload dies. There's no separate list of "banned" types that SVG happens to be on — it simply was never added.

You can confirm this on your own install in one line. Grep core for the string and you get nothing back:

```
grep -n "'svg" wp-includes/functions.php
# no matches
```

The image section of that array runs from JPEG, GIF, PNG, and BMP through TIFF, WebP, AVIF, ICO, and the HEIC family. All raster. Every one of them is a container for pixel data, and pixel data can't do anything except be pixels.

### SVG isn't an image file, it's a document

That's the whole reason for the split. A PNG is a compressed grid of colour values; an SVG is XML, a text document the browser parses, builds a DOM from, and renders. It can carry `<style>` blocks, external references, and, if you put one there, a `<script>` element. The [SVG 2 specification's chapter on scripting and interactivity](https://www.w3.org/TR/SVG2/interact.html) exists precisely because scripting is a first-class feature of the format, not an abuse of it.

Uploading an SVG is closer to uploading an HTML file than a photo, and core already refuses those too: `get_allowed_mime_types()` strips `htm|html` and `js` from the list for any user without the `unfiltered_html` capability, right alongside dropping `swf` and `exe` for everyone.

**WHY IT MATTERS**

Anyone who can upload media on your site can put a file in your uploads directory that is served from your domain. For a JPEG that's harmless. For a text document the browser will parse, and will execute in some contexts, it means the trust boundary isn't the upload form — it's **every account that holds the upload\_files capability**, including the author account you added once and forgot about.

## The Snippet Everyone Pastes, and What It Actually Does

The fix circulating everywhere hooks the [`upload_mimes`](https://developer.wordpress.org/reference/hooks/upload_mimes/) filter and adds a key:

```
add_filter( 'upload_mimes', function ( $mimes ) {
    $mimes['svg'] = 'image/svg+xml';
    return $mimes;
} );
```

That's the correct hook, and it does work. But it's worth understanding why filtering is what opens the gate, because there's a near-identical approach that looks like it should work and silently doesn't.

### Passing the MIME type as an argument gets you nothing

`wp_check_filetype_and_ext()` takes an optional third parameter, `$mimes`, and you'd reasonably expect handing it an SVG mapping to be equivalent to filtering. It isn't. I ran three variants against the same file on a stock WordPress 7.1 install; the first two hold the filter constant and vary only the argument:

```
// 1. Baseline — no filter, no third argument:
wp_check_filetype_and_ext( '/tmp/t.svg', 't.svg' );
// => ext => false, type => false

// 2. No filter, third argument supplied — the only thing that changed:
wp_check_filetype_and_ext( '/tmp/t.svg', 't.svg', array( 'svg' => 'image/svg+xml' ) );
// => ext => false, type => false        <- the argument changed nothing

// 3. upload_mimes filter hooked, no third argument:
wp_check_filetype_and_ext( '/tmp/t.svg', 't.svg' );
// => ext => 'svg', type => 'image/svg+xml'
```

The reason is a final gate near the end of that function. After all the sniffing is done, core runs one last check, and it calls the allowlist with no arguments at all:

```
// The mime type must be allowed.
if ( $type ) {
    $allowed = get_allowed_mime_types();

    if ( ! in_array( $type, $allowed, true ) ) {
        $type = false;
        $ext  = false;
    }
}
```

Because `get_allowed_mime_types()` is called bare, your `$mimes` argument never reaches it. That function builds its list from `wp_get_mime_types()` and then applies the `upload_mimes` filter. Filtering is the way in, whether through `upload_mimes` or the broader `mime_types` filter that feeds it. What you can't do is pass the type as an argument, and if you've ever wondered why some SVG snippets on the internet don't work, this is usually why.

## What Core Actually Checks, and What It Doesn't

There's a common belief that WordPress just trusts the file extension. It doesn't, and the real behaviour is more interesting.

When you upload something, `wp_check_filetype_and_ext()` tries to work out what the file really is. For anything claiming to be an image it calls `wp_get_image_mime()`, which leans on `exif_imagetype()`. That returns `false` for SVG, since there's no binary image header to read. Core then falls through to a second test using PHP's `fileinfo` extension, and that one does identify it:

```
finfo says:          image/svg+xml
exif_imagetype:      false
wp_get_image_mime:   false
```

So core genuinely inspects the bytes. The declared and detected types match, the file passes that stage, and then it hits the allowlist gate and gets rejected there. Rename a PHP script to `logo.svg` and `fileinfo` reports it as text or PHP rather than SVG, so the mismatch branch catches it — that protection is real and worth knowing you have.

### The check that isn't happening

Here's what that same detection does with a hostile file. I wrote an SVG with a script element inside it and ran it through the identical code path, with the `upload_mimes` filter in place:

```
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg">
  <script>alert(1)</script><circle r="5"/>
</svg>

evil.svg  finfo=image/svg+xml  => ext='svg' type='image/svg+xml'
```

Identical result to the clean file under those same conditions. Core isn't checking whether the file is safe; it's checking whether the file is what it says it is. A scripted SVG *is* a valid SVG, so it sails through, and no amount of MIME sniffing will ever separate the two. That distinction is the entire reason the allowlist exists — once you open it, validation stops being core's job and starts being yours.

### unfiltered\_upload is not the escape hatch it looks like

You'll see `unfiltered_upload` mentioned as the capability that lets an administrator upload anything. The administrator role does carry it — `populate_roles_230()` in `wp-admin/includes/schema.php` adds it. But checking whether an actual admin can use it returns something different:

```
administrator role has the cap in DB:  true
user_can( 1, 'unfiltered_upload' ):    false
```

The capability is intercepted in `map_meta_cap()`, which denies it outright unless the `ALLOW_UNFILTERED_UPLOADS` constant is defined and true in `wp-config.php` (and on multisite, unless you're a super admin). It's off by default on every standard install, so it's not the mechanism making your SVGs work, and turning it on is a considerably bigger hammer than adding one MIME type.

## The Part Most SVG Advice Gets Wrong

Search for SVG security and you'll be told that displaying a user-uploaded SVG can run JavaScript in your visitors' browsers. As a blanket statement, that's not accurate, and the specification is unusually clear about it.

SVG 2 defines several [processing modes](https://www.w3.org/TR/SVG2/conform.html), and which one applies depends entirely on how the file is embedded. For secure static mode and secure animated mode — the two that apply to images — the spec lists the features in a table, and script execution is listed as **no** in both. The rule for embedded files is explicit:

> An SVG embedded within an 'image' element must be processed in secure animated mode if the embedding document supports declarative animation, or in secure static mode otherwise. The same processing modes are expected to be used for other cases where SVG is used in place of a raster image, such as an HTML 'img' element or in any CSS property that takes an &lt;image&gt; data type.

The `<image>`-element rule is a normative requirement; the spec extends the same modes to the HTML `<img>` element and CSS image values as an expectation, tying it back to HTML's own rule that an image source must be "a non-interactive, optionally animated, image resource that is neither paged nor scripted." That's what browsers implement, so an SVG sitting in a normal `<img>` tag on your homepage isn't running anything.

### Where it does run

The risk moves to two other places. The first is the file's own URL. Every Media Library upload gets a public address like `/wp-content/uploads/2026/09/icon.svg`, and when a browser navigates there directly the file is no longer a sub-resource. The spec says a document viewed directly is displayed "using the most comprehensive processing mode supported by the user agent" — dynamic interactive mode, where scripting is on.

That's one attack: get a scripted SVG into the uploads folder, then get a logged-in administrator to open that URL, and the script runs on your domain with that admin's session.

The second is embedding. The spec adds that the same rules apply when an SVG loads in an HTML `embed`, `iframe`, or `object` element, and that a top-level browsing context in an interactive browser "is equivalent to SVG's dynamic interactive processing mode." So a scripted SVG dropped into an un-sandboxed `<iframe>` runs without anyone navigating anywhere. Both routes need the file rendered outside a plain image context, which is why the icon set we found had never caused a visible symptom — it was only ever referenced in `<img>` tags.

**SCRIPTING BY CONTEXT (PER SVG 2)**

`<img src="logo.svg">` — secure static or secure animated mode. Scripting: no.

`background-image: url(logo.svg)` — same secure modes as an image. Scripting: no.

`<iframe>`, `<embed>`, `<object>` — inherits the embedding document's mode, which on an ordinary page is dynamic interactive mode. Scripting: yes, unless you sandbox it.

**Opening the file URL directly** — the most comprehensive mode the browser supports. Scripting: yes.

## How to Ship SVG Without Leaving the Door Open

None of this means you can't use SVG — vector logos are sharp at every resolution and scale without a srcset. You just need to supply the validation core never performed, because until you filtered the allowlist it refused the format outright.

### Sanitize on upload, not on display

The right fix is to strip scripts, event handlers, and external references from the file before it's written to disk. Do it at upload time so the bad markup never lands in your uploads directory, rather than trying to filter it on the way out — output filtering only protects the contexts you remembered to filter, and direct file access isn't one of them.

Practically, this means a plugin or a library built for the job. A general-purpose security plugin won't do it, and neither will WordPress's own `wp_kses()`, which is built for HTML and doesn't know the SVG element and attribute vocabulary. Look for something that explicitly sanitizes SVG, and confirm it strips `on*` event attributes, not just `<script>` tags. The file we found used `onload`, which a naive script-tag-only filter would have sailed straight past.

### Limit who can upload them

Adding the MIME type opens SVG uploads for every role that can upload files, which by default means administrators, editors, and authors. If only administrators ever ship a logo, gate the filter behind a capability check so the allowlist only widens for the people who need it. Swap `manage_options` for whichever capability your trusted role actually holds:

```
add_filter( 'upload_mimes', function ( $mimes ) {
    if ( current_user_can( 'manage_options' ) ) {
        $mimes['svg'] = 'image/svg+xml';
    }
    return $mimes;
} );
```

### Close the direct-navigation path

Serving uploaded SVGs with a `Content-Disposition: attachment` header is the other common hardening step. Per [MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Content-Disposition), that header tells the browser to download the file rather than display it inline, which closes the direct-navigation case. Be aware it applies to anyone hitting that URL, so test it against how your theme references the file before rolling it out.

**OPEN SOURCE**

We build and maintain a free, MIT-licensed suite of WordPress plugins covering security hardening, structured data, SEO meta, and cookie consent. SVG sanitization needs a dedicated sanitizer rather than a general security plugin, so treat that as a separate tool — but if you're auditing what else is loose on a site, the suite is a reasonable place to start.

[View on GitHub ](https://github.com/abchiaravalle/amplifi.plugins)

## What You Lose Even When It Works

Once SVG uploads are enabled the file goes into the Media Library, but it doesn't behave like the other images there, and this surprises people more than the security question does.

Core decides whether to generate thumbnails using `file_is_displayable_image()`, which checks the detected image type against a fixed list of GIF, JPEG, PNG, BMP, ICO, WebP, and AVIF. SVG isn't on it, and the function returns `false`. That result forms part of the resize gate in `wp_generate_attachment_metadata()`, which fires only for `image/heic` or for an `image/*` type that passes the displayable check. SVG satisfies neither, so no intermediate sizes are created.

So: no thumbnail, no medium or large variants, no `srcset`, and often no width or height in the attachment metadata. Themes that read those dimensions to reserve layout space can end up with a collapsed or oddly-sized logo, and the Media Library grid may show a generic file icon instead of a preview. None of it is broken exactly, but if a client asks why the new logo looks wrong in the grid, this is why.

SVG also isn't automatically the lighter choice. A complex illustration with thousands of path nodes can easily outweigh a well-compressed PNG, and unlike a raster file it costs parse and render time rather than just bytes. For logos, icons, and simple diagrams it's an easy win. For anything photographic or highly detailed, it isn't.

## Frequently Asked Questions

Why does WordPress block SVG uploads by default?WordPress accepts uploads based on an allowlist of extensions and MIME types, and SVG was never added to it. The reason is that SVG is an XML document the browser parses rather than a container of pixel data, so it can carry scripts, event handlers, and external references. Core treats it much like it treats HTML and JavaScript files, which are also withheld from users who lack the unfiltered\_html capability.

Does an SVG in an img tag run JavaScript?No. The SVG 2 specification requires an SVG embedded in an image element to be processed in secure static mode or secure animated mode, and both list script execution as unavailable. The same applies when an SVG is used in a CSS property that takes an image value. Scripting becomes available in two other contexts: when the file is opened directly at its own URL, because a directly viewed document uses the most comprehensive processing mode the browser supports, and when it is loaded in an un-sandboxed embed, iframe, or object element, which inherits the embedding page's dynamic interactive mode.

Is it safe to allow SVG uploads on my site?It can be, provided you supply the validation core was never doing. Enabling the MIME type alone means core will accept a scripted SVG exactly as readily as a clean one, because both are genuinely valid SVG files. Sanitize files at upload time to strip scripts, event handlers, and external references, and restrict the filter so only trusted roles can upload vector files at all.

Why does my SVG have no thumbnail or srcset?Core generates intermediate image sizes only for HEIC files or for image types that pass its displayable-image check, and that check compares against a fixed list covering GIF, JPEG, PNG, BMP, ICO, WebP, and AVIF. SVG is on neither path, so the resize step is skipped entirely and no thumbnail, medium, or large variants are produced. Width and height are frequently absent from the attachment metadata as well, which is why some themes size an SVG logo incorrectly.

Do I need the unfiltered\_upload capability to upload SVG?No, and on a standard install you could not use it anyway. The administrator role does carry that capability in the database, but core intercepts the check and denies it unless the ALLOW\_UNFILTERED\_UPLOADS constant is defined and true in wp-config.php, with an additional super admin requirement on multisite. Filtering the allowed MIME types is the supported route, and it is far narrower than switching off upload filtering altogether.

Does WordPress check the file contents or just the extension?It checks both. Core reads the actual bytes using the fileinfo extension and compares the detected MIME type against the one implied by the extension, rejecting the upload when they disagree. That is what stops a PHP script renamed with an image extension. What it cannot do is judge intent, because a scripted SVG and a clean SVG are both detected as the same MIME type, so content sanitization has to happen separately.

If your site has SVG uploads enabled and you're not sure what's already in the uploads folder, it's worth an afternoon to find out — grep the directory for script tags and on-event attributes before you go looking for anything more exotic.

Built by [amplifi.studio](https://amplifi.studio) — see also [One Login Away From Disaster: A WordPress Security Hardening Guide](https://amplifi.studio/wordpress-security-hardening-checklist-guide/).