WordPress Images

WordPress SVG Upload: Why It Is Blocked and How to Fix It

WordPress refuses SVG uploads out of the box, and the snippet everyone pastes works on some servers and not on others. Here is the vector case for wanting one, the exact core check that rejects it, and the sizing bug you inherit once the upload finally succeeds.

WordPress SVG Upload: Why It Is Blocked and How to Fix It

Somebody sends you the logo. It is a PNG, 400 pixels wide, with a white box around it. You need it in the header at twice that size. You ask for the SVG, it arrives, you drag it into the media library, and WordPress tells you that you are not allowed to upload this file type.

That refusal is deliberate. A WordPress SVG upload is blocked by core itself, and it is one of the few WordPress defaults that is a security decision rather than a historical accident. The advice you find for getting around it is mostly a four line snippet that does not work on half of the servers it is pasted into, and the people it does work for are usually not told what they just switched on.

Both halves are worth knowing: why you want a vector, and what actually happens inside WordPress when you try to upload one. The second half holds the real surprises, because even a successful upload leaves you with an attachment that core cannot measure, resize or describe.

Two completely different things

A raster image is a grid of colored squares. A photograph, a screenshot, a PNG, a JPEG. It knows the color of every pixel and nothing else. Ask it to be twice as big and it has to invent the pixels in between, which is why enlarged pictures go soft.

A vector image is a set of instructions. A circle of this radius at this position, a curve through these points, filled with this color. It has no resolution at all. Ask it to be twice as big and it simply draws the same instructions at a different scale, perfectly sharp, at any size from a favicon to the side of a building.

That is the entire difference, and everything below follows from it, including the security question.

What each one is for

Vector is for anything drawn: logos, icons, illustrations with flat color, diagrams, type, patterns, anything that gets reproduced at more than one size. SVG on the web, PDF or EPS for print.

Raster is for anything captured or continuously toned: photographs, screenshots, textures, anything painted with a brush, anything with real world noise in it.

The mistakes go both ways. A logo stored only as PNG is the common one. The reverse happens too: someone auto-traces a photograph into a vector and gets a fifteen megabyte file made of forty thousand shapes that looks worse than the original and brings the browser to its knees.

What a vector gives you that people forget

  • Any size, forever. The obvious one. One file works for a favicon and a trade show banner.
  • Tiny files for flat artwork. A logo as SVG is often a couple of kilobytes against a hundred for the PNG version, and it is sharp on every screen instead of only one.
  • Editable after the fact. Colors, shapes and text stay separate objects. Recoloring a logo for a dark background is a click rather than a rebuild.
  • Styleable in the browser. An inline SVG can be recolored with CSS, animated, or made to react to a theme switch. A PNG cannot.
  • Real transparency, no edges. No halo, no matte color baked in, no surprise white box.

The fourth point is the one that matters for the rest of this article. An SVG can be recolored with CSS and animated with JavaScript because an SVG is not a picture, it is a document. Open one in a text editor and you can read it. That is the source of every advantage above, and it is also the reason WordPress will not take one from you by default.

Table comparing raster and vector files across four questions, showing that only the vector scales to any size, can be recolored with CSS and can carry a script element, while only the raster holds a photograph well.

WordPress SVG upload is refused by default

This is not a setting you have missed and it is not your host. WordPress keeps a hardcoded map of file extensions to mime types in wp_get_mime_types(), in wp-includes/functions.php. The image section of that array is short:

// wp-includes/functions.php, inside wp_get_mime_types()
// Image formats.
'jpg|jpeg|jpe' => 'image/jpeg',
'gif'          => 'image/gif',
'png'          => 'image/png',
'bmp'          => 'image/bmp',
'tiff|tif'     => 'image/tiff',
'webp'         => 'image/webp',
'avif'         => 'image/avif',
'ico'          => 'image/x-icon',
'heic'         => 'image/heic',
'heif'         => 'image/heif',

There is no svg key and no image/svg+xml value. The string svg does not occur anywhere in that file. Search the whole of wp-includes and wp-admin/includes for image/svg and the only hits are documentation comments about admin menu icons, where a base64 data URI is passed to register_post_type(). Core has no SVG upload handling, no SVG sanitiser, and no SVG branch in the media pipeline at all. The format simply is not there.

get_allowed_mime_types() takes that array, removes swf and exe unconditionally, and then removes htm|html and js unless the user has the unfiltered_html capability. Only after that does it run the upload_mimes filter. That sequence tells you how core thinks: file types that can carry markup or script are a capability question, not a format question.

Why SVG is treated as markup, not as an image

A JPEG that you serve to a browser is handed to an image decoder. Whatever nonsense is inside it, the worst realistic outcome is a broken picture or a decoder bug. An SVG served to a browser is parsed by the same engine that parses your pages. It can contain a <script> element. It can contain event attributes like onload. It can reference external files, embed a foreign object, or pull in a stylesheet.

When that file lives on your own domain and someone opens it directly, the script inside it runs in your site’s origin. That is the precise risk, and it is worth stating precisely because it is routinely both overstated and dismissed. It is not that an SVG in a page will take over your server. It is that an upload form which accepts SVG is an upload form which accepts arbitrary markup, executing under your domain, stored at a predictable URL.

How much that matters depends entirely on who can upload. If you are the only account on the site, an SVG you exported yourself is about as dangerous as the PNG next to it. If contributors, a membership plugin, a job board or any front end form can put a file on your server, the calculation changes, and the honest answer is often to leave SVG uploads off.

Why the snippet from the forums does not work

The answer that circulates everywhere adds the type through the upload_mimes filter and stops there. It reliably produces one of two outcomes: it works perfectly, or the upload is refused with exactly the same message as before and nobody can explain why. Both outcomes come from the same code path.

Uploads go through wp_check_filetype_and_ext(), also in wp-includes/functions.php. It runs in two stages. Stage one calls wp_check_filetype(), which matches the filename against get_allowed_mime_types(). This is where the upload_mimes filter has effect, so after the snippet, logo.svg resolves to ext = svg and type = image/svg+xml. So far so good.

Stage two is the part the snippet does not survive. The next line in that function is if ( $type && str_starts_with( $type, 'image/' ) ), and inside it core calls wp_get_image_mime() to find out what the file really is. That function tries exif_imagetype(), falls back to getimagesize(), and then reads twelve bytes to check the magic sequences for WebP and AVIF. None of those recognise SVG, because SVG has no binary header to recognise. So $real_mime comes back false.

Core then falls through to the generic validator, which asks PHP’s fileinfo extension instead. The structure of what follows is the whole story, so here it is with the branch bodies removed:

// wp-includes/functions.php, inside wp_check_filetype_and_ext()

// Validate files that didn't get validated during previous checks.
if ( $type && ! $real_mime && extension_loaded( 'fileinfo' ) ) {
    $finfo     = finfo_open( FILEINFO_MIME_TYPE );
    $real_mime = finfo_file( $finfo, $file );

    if ( in_array( $real_mime, $nonspecific_types, true ) ) {
        // Non specific binary types, forgiven for application, video and audio.
    } elseif ( str_starts_with( $real_mime, 'video/' )
        || str_starts_with( $real_mime, 'audio/' ) ) {
        // Media types, only the major type has to match.
    } elseif ( 'text/plain' === $real_mime ) {
        // Forgives text/csv, application/csv, text/richtext, text/tsv, text/vtt.
    } elseif ( 'text/rtf' === $real_mime ) {
        // Special casing for RTF files.
    } else {
        if ( $type !== $real_mime ) {
            /*
             * Everything else including image/* and application/*:
             * If the real content type doesn't match the file extension,
             * assume it's dangerous.
             */
            $type = false;
            $ext  = false;
        }
    }
}

Every branch of leeway in that chain is for something other than an image. Non specific binary types are forgiven for application, video and audio extensions. Real mime types beginning with video/ or audio/ only have to match on the major type. A file reported as text/plain is forgiven if the expected type is CSV, TSV, VTT or richtext, and image/svg+xml is not on that list. An image type falls into the final else, where the string fileinfo reports has to be character for character identical to the type derived from the extension. Anything else and both $type and $ext become false, and the upload is refused as dangerous.

Which explains the coin flip. What fileinfo reports for an SVG depends on how the file begins, because libmagic identifies text formats by matching patterns near the start of the file rather than by reading a header.

A file that starts with an XML declaration, <?xml version="1.0" encoding="UTF-8"?>, followed by an <svg> root element, matches libmagic’s SVG rule and is normally reported as image/svg+xml. That equals the type derived from the extension, the comparison passes, and the upload succeeds. This is the file that made the snippet look like it works.

A file that starts directly with <svg xmlns="http://www.w3.org/2000/svg">, with no XML declaration, may not be recognised as SVG at all. Depending on the libmagic version installed on the server it can come back as text/xml, text/plain or text/html. None of those equals image/svg+xml, none of them reaches a forgiving branch, and the file is refused.

Most design tools omit the XML declaration in their default web export, and most optimisers strip it to save bytes. That is why two logos out of the same design file, exported a week apart, can behave completely differently on the same site. It is also why plugins that do this properly do not stop at upload_mimes: they hook wp_check_filetype_and_ext as well and put the extension and type back for .svg files after inspecting the contents themselves.

Flow diagram of the WordPress upload check for a file named logo.svg, showing the filename resolving to image svg plus xml, wp_get_image_mime returning false, fileinfo being asked instead, and the upload either accepted when the file starts with an XML declaration or refused when it does not, with two code panels showing both file openings.

The pattern that is actually safe

Getting the file past the gate is the easy part. Making it safe to have is four decisions, and only the first one is code.

Sanitise every file on upload. Not just the ones from strangers, every file. A sanitiser parses the SVG, walks the node tree, and strips script elements, event handler attributes, external references and anything else not on an allowlist. This needs updating as browsers add features, which is why it should be a maintained library and not a regular expression you wrote yourself. If you cannot tell from a plugin’s description whether it sanitises, that is your answer.

Restrict who can upload the type. Core already models this for HTML and JavaScript through unfiltered_html, and SVG belongs in the same category. If your site has editors, authors, contributors, subscribers who can submit, or any front end upload form, narrow the type to administrators.

Consider not enabling it at all. On a multi author site, or anywhere the upload path is exposed to people outside your organisation, a PNG exported at 2x or 3x is an unglamorous answer that removes the problem. You lose infinite scaling and CSS recoloring. You also lose an attack surface.

Know what is already on the server. Enabling the type is not a one way door you can forget about. The site you are reading this on has 35 attachments with the mime type image/svg+xml, which means SVG uploads are enabled here, by a plugin, which is how it happens on essentially every site that has them. Nothing in core put them there.

If you already have an SVG plugin running and want to narrow it to administrators, this is the filter. It belongs in a small site plugin or in a child theme’s functions.php, and the late priority matters so that it runs after whatever enabled the type in the first place.

/**
 * Narrow an existing SVG upload plugin to administrators only.
 *
 * This does NOT sanitise anything. It reduces who is allowed to try,
 * which is worth doing, and it is not a substitute for a sanitiser.
 */
add_filter(
    'upload_mimes',
    function ( $mimes, $user = null ) {
        $can_upload = $user
            ? user_can( $user, 'manage_options' )
            : current_user_can( 'manage_options' );

        if ( ! $can_upload ) {
            unset( $mimes['svg'], $mimes['svgz'] );
        }

        return $mimes;
    },
    99,
    2
);

I have deliberately not printed the three line version that only adds the type. Opening the door is not the hard part, and it is not the part that deserves a copy button.

If you do allow SVG, the file has to be clean before it goes anywhere near the media library. The cleaner below strips scripts, event handlers, external references and the megabytes of editor leftovers that Illustrator and Figma leave behind, and shows you exactly what it took out.

SVG cleaner

Paste SVG source or drop a .svg file, tick what should go, and get a smaller, safer file back. Everything happens in this browser tab: nothing is uploaded.

Preview of the cleaned file
Nothing to preview yet
Safety
Cleanup
Numbers and geometry
Nothing pasted yet

The file is parsed as XML, so a broken tag is reported instead of being silently mangled. Shapes are never redrawn or merged and ids are kept, so anything your CSS or JavaScript points at still works.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

What happens after the upload succeeds

This is the part almost nobody writes about, and it is the reason so many people enable SVG uploads, get the logo in, and then spend an afternoon wondering why it renders eight pixels tall.

Every upload goes through wp_generate_attachment_metadata() in wp-admin/includes/image.php. Its first branch reads:

if ( 'image/heic' === $mime_type
    || ( preg_match( '!^image/!', $mime_type )
        && file_is_displayable_image( $file ) ) ) {
    // Make thumbnails and other intermediate sizes.
    $metadata = wp_create_image_subsizes( $file, $attachment_id );
}

An SVG passes the image/ regex and then fails file_is_displayable_image(), which calls wp_getimagesize() and checks the result against a fixed list of GIF, JPEG, PNG, BMP, ICO, WebP and AVIF. SVG is in none of them. The branch is skipped, the file is not video and not audio, and $metadata stays the empty array it was initialised as. The only thing core adds after that is the file size, from a late if ( ! isset( $metadata['filesize'] ) ) check.

So core produces no sub-sizes for an SVG. No thumbnail, no medium, no large, no registered custom sizes, and no sizes key in the metadata at all. That is not a bug, it is correct: there is nothing to resize, one SVG file already serves every size. But a great deal of WordPress assumes that key exists.

Here is the stored metadata for the WunderPaint logo on this site, attachment 2650, an SVG uploaded through the media library like any other file:

$ wp post meta get 2650 _wp_attachment_metadata --format=json

{"filesize":4190,"width":96.03,"height":18.83}

Two things to notice. There is no sizes array, as expected. And the width and height are fractional. Those are not pixels. They are the numbers out of the file’s viewBox attribute, and core did not put them there, because core would have stored the file size and nothing else. The plugin that enabled the upload added them. An SVG’s viewBox is a coordinate space, not a pixel dimension: a logo drawn on a 96.03 by 18.83 grid is exactly as valid as the same logo drawn on a 960 by 188 grid, and the two files render identically.

Now trace what WordPress does with that. wp_attachment_is_image() returns true, because wp_attachment_is() checks whether the post mime type starts with image/ and returns early, long before the extension allowlist further down. So image_downsize() treats the SVG as an image, finds no intermediate size to use, and reaches its last resort: $width = $meta['width']. That value travels straight into wp_get_attachment_image(), which assigns it to the attribute without rounding it.

The result is an img tag carrying width="96.03" height="18.83". A theme that sizes its logo in CSS will not care. A theme that respects the attributes, or a layout that uses them to reserve space, renders a logo the size of a postage stamp. If the viewBox has an aspect ratio the theme does not expect, you get the stretched version instead. It is a distinct failure from the usual reasons a WordPress image looks wrong, because the file itself is perfect and only the numbers around it are meaningless.

There is no srcset either. wp_calculate_image_srcset() returns false as soon as $image_meta['sizes'] is empty, and an SVG has no sizes, so the responsive image machinery quietly does nothing. This is the correct outcome and worth understanding rather than debugging, because an audit plugin that reports no srcset on your logo is telling you the truth about a file that does not need one.

The practical fix is a line of CSS rather than a filter. Size the SVG yourself and let the attributes be wrong:

.site-logo img[src$=".svg"] {
    width: 180px;
    height: auto;
}
Table showing that a PNG upload gets sub size files, a sizes array, pixel dimensions and a srcset while an SVG upload gets none of them, above the stored metadata for attachment 2650 reading filesize 4190, width 96.03 and height 18.83, and the resulting img tag carrying those fractional numbers.

When you only have the raster version

Sometimes the original genuinely no longer exists. The agency closed, the designer left, the file is on a drive nobody can find. You have a 400 pixel PNG and a deadline.

Vectorise it. Automatic tracing looks at the pixels and produces shapes. For a flat logo with clean edges and a handful of colors, modern tracing is genuinely good. For anything with gradients, soft edges or fine detail, it produces a mess that looks fine at thumbnail size and falls apart when you zoom. Compare the trace against the original at large size before you trust it. The typical failure is corners going slightly round and the counters of letters filling in.

Upscale it. Modern upscaling reconstructs plausible detail rather than only interpolating, and for photographs it is remarkable. For a logo it is the wrong tool, because plausible is not the same as correct and a logo has to be exactly right.

Rule of thumb: photograph too small, upscale. Logo too small, vectorise, then clean up the result by hand. WunderPaint’s editor imports SVGs as editable vector layers and exports your design as SVG again, which is the part of this job a browser based editor is genuinely good at.

Clean the file before you upload it either way. Exports from design software carry editor metadata, empty groups and absurd decimal precision, and an optimiser routinely halves the file with no visible change. Convert text to outlines while you are there, because an SVG that references a font renders with whatever the viewer happens to have installed.

Symptom and cause

“Sorry, this file type is not permitted for security reasons” and you have not touched anything. This is the default. image/svg+xml is not in wp_get_mime_types(), so get_allowed_mime_types() never returns it and the extension never matches.

You added the upload_mimes filter and the upload is still refused. Stage two of wp_check_filetype_and_ext() asked fileinfo what the file really is, got something other than image/svg+xml, and set both the type and the extension to false. Adding an XML declaration to the top of the file often fixes it, which tells you the check was never really about the extension.

One SVG uploads and a nearly identical one does not. Same cause. One export has the XML declaration, the other does not, and libmagic matches patterns at the start of the file. Nothing about the artwork is different.

The logo uploads fine and renders tiny, or stretched. The attachment carries width and height taken from the viewBox rather than from pixels, and those numbers land directly in the img tag. Size it in CSS and let the attributes be wrong.

The media library shows a generic file icon instead of a preview. No sub-sizes were generated, because file_is_displayable_image() rejects SVG and wp_create_image_subsizes() is never called. There is no thumbnail file for the admin to show, so unless the plugin that enabled the type supplies a preview of its own, you get the icon.

An audit tool reports that the logo has no srcset. Correct and harmless. wp_calculate_image_srcset() returns false when the metadata has no sizes array, and a vector needs no responsive variants.

The shape of the decision

The vector question and the WordPress question are the same question seen from two ends. A vector is better than a raster for a logo because it is a document rather than a picture, and WordPress refuses it for exactly the same reason. You do not get the infinite scaling and the CSS recoloring without also getting a file format that can carry a script tag. There is no version of SVG that has the first property and not the second.

So the decision is not really about SVG, it is about your upload surface. If you are the only person who can put files on the server, enable the type with a plugin that sanitises, upload your own cleaned exports, and get on with your day. If other people can upload, or you are not certain whether they can, treat SVG the way core treats HTML and JavaScript: administrators only, sanitised on the way in, or not at all.

And whichever way you go, ask for the vector source the moment a logo or an icon set is made. Not the export, the source. Store it somewhere findable, next to the brand colors and the fonts, and generate the PNGs from it whenever you need them. That one email at the right moment saves an afternoon at the wrong one, which is my favorite kind of trade, and it is the only part of this article that would still be true if WordPress changed its mind tomorrow.

WordPress SVG Upload: Why It Is Blocked and How to Fix It

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Troubleshooting

WordPress Missed Schedule: Why WP-Cron Is Not a Cron Job

WP-Cron has no timer and no background process. It is a list of due tasks that gets checked only when somebody loads a page, which explains missed post schedules, stalled backups, and every other scheduled task that silently stops.

Security & Privacy

WordPress Salts and Passwords: The Eight Lines Nobody Rotates

The eight define() lines in wp-config.php sign every admin cookie and every nonce your site issues. What each one does, why Math.random cannot be trusted to make them, what actually breaks when you rotate them, and an offline generator for the replacements.

Photo Editing

Dithering Side by Side: Why Atkinson Looks Like a Mac and Bayer Like a Game

Eight dithering methods on one picture, with the same palette and the same options. Every kernel and divisor written out, the Bayer matrix built by recursion, and the linear light step that almost every quick implementation skips.

AI Images

AI Product Photos That Do Not Look Fake: Light, Shadow, Perspective

A real product photo on a generated background gives itself away on exactly three axes: light direction, shadow quality and perspective. Here is the physics in plain words, a browser tool that walks you through all three checks on your own composite, and the prompt lines that stop the mismatch happening in the first place.

WordPress Images

WordPress Replace Image: Swap the File, Keep Every Link

WordPress has no replace function, because an attachment is a path plus a cached description of whatever sits at that path. This is why a re-upload becomes hero-1.jpg, what an SFTP overwrite really leaves behind, and the order of operations that swaps a file without breaking twenty references.

WordPress Development

WordPress Child Themes: When You Need One and When You Don’t

A child theme is the right answer to one problem: overriding template files in a theme that still receives updates. For CSS tweaks and a few PHP snippets, it is overhead you maintain forever for no benefit.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

Pro

3D Textile Studio

Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

Pro

3D Particle Studio

Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.

Free

Handwriting Fonts

Draw the alphabet here or fill in a printed sheet and photograph it. What comes out is a genuine font family, installed into your site and available in every picker.