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 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.
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 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, 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 <image> 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.
<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, 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.
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 GitHubWhat 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
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 — see also One Login Away From Disaster: A WordPress Security Hardening Guide.