You drop a crisp SVG into the WordPress media library. Instead of a sharp icon or a clean logo, you get a broken image, a security error, or a blank thumbnail. The frustration is real. WordPress does not natively support SVG uploads out of the box, and the reasons are buried in a mix of security architecture and XML parsing quirks. I’m Jonas Venn, and I spend my days untangling the systems that make WordPress sites tick. This is a hands-on, no-plugin guide to understanding exactly what breaks your SVGs and how to patch the behavior directly.

What Actually Happens When You Upload an SVG
An SVG is not an image in the traditional sense. It is an XML document that describes shapes, paths, and styles. When you drag that file into the WordPress media uploader, the system runs a series of checks. The first gatekeeper is the MIME type validator. WordPress looks at the file extension and the detected MIME type. For SVGs, the correct MIME is image/svg+xml. If your server’s finfo detection or the browser’s reported type does not match, WordPress rejects the file outright. You will see the generic “Sorry, this file type is not permitted for security reasons” message.
Even if the MIME check passes, the file still hits the XML parser. WordPress uses PHP’s simplexml_load_string() to inspect the SVG’s structure. If the SVG contains malformed XML, unclosed tags, or embedded scripts, the parser fails silently or throws an error. The upload dies. This is by design: SVG files can carry JavaScript, foreign objects, and event handlers. A malicious SVG could execute code in the browser of anyone viewing your media library or a page where the file is embedded. The core team’s stance is conservative, and for good reason.
The MIME Type Mismatch That Kills Your Upload
Most broken SVG uploads fail at the MIME detection layer. WordPress relies on PHP’s finfo_file() function, which reads the file’s magic bytes. A valid SVG should start with an XML declaration or an <svg> tag. If your SVG was exported from a design tool that added a Byte Order Mark (BOM) or unusual whitespace, the MIME detector might return text/plain or application/octet-stream. WordPress sees that, compares it against the allowed list, and blocks the upload.
You can verify this yourself. Open the SVG in a code editor, not a vector tool. Look at the very first characters. They should be <?xml version="1.0" encoding="utf-8"?> or <svg xmlns="http://www.w3.org/2000/svg". If you see blank lines or strange characters before the XML declaration, delete them. Save the file with UTF-8 encoding without BOM. This single cleanup step resolves a surprising number of upload failures.
On the server side, you can also force WordPress to trust the file extension when the MIME check is unreliable. This is not a plugin. It is a small filter added to your theme’s functions.php or a site-specific mu-plugin. The filter intercepts the MIME check and explicitly maps .svg to image/svg+xml. Here is the exact code:
add_filter('wp_check_filetype_and_ext', function($data, $file, $filename) {
$ext = pathinfo($filename, PATHINFO_EXTENSION);
if ($ext === 'svg') {
$data['type'] = 'image/svg+xml';
$data['ext'] = 'svg';
}
return $data;
}, 10, 3);
This filter does not bypass security. It only corrects a misdetection. The file still goes through the XML sanitization step, which we will address next.
XML Sanitization: The Hidden Gatekeeper
WordPress 4.7.1 introduced an SVG sanitization library, kses, to strip dangerous tags and attributes from uploaded SVGs. The problem is that the sanitizer is strict. It removes <style> blocks, <script> tags, event handlers like onload, and even some valid XML namespaces. Your beautifully crafted SVG with embedded CSS or custom data attributes gets gutted. The result is a broken graphic or a file that fails validation entirely.
You can see this in action. Upload an SVG with a <style> element. Check the file in the media library. The styles are gone. The sanitizer runs through wp_handle_upload and uses the wp_kses framework. If you need to preserve inline styles or specific SVG features, you must extend the allowed tags and attributes. This is done with a filter on wp_kses_allowed_html. Add this to your functions.php:
add_filter('wp_kses_allowed_html', function($allowed, $context) {
if ($context === 'post') {
$allowed['svg'] = array(
'xmlns' => true,
'viewbox' => true,
'width' => true,
'height' => true,
'fill' => true,
'stroke' => true,
'class' => true,
'id' => true,
);
$allowed['style'] = array('type' => true);
$allowed['path'] = array(
'd' => true,
'fill' => true,
'stroke' => true,
'stroke-width' => true,
'opacity' => true,
);
$allowed['circle'] = array('cx' => true, 'cy' => true, 'r' => true, 'fill' => true, 'stroke' => true);
$allowed['rect'] = array('x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'fill' => true, 'stroke' => true);
$allowed['line'] = array('x1' => true, 'y1' => true, 'x2' => true, 'y2' => true, 'stroke' => true);
$allowed['g'] = array('fill' => true, 'stroke' => true, 'transform' => true);
$allowed['use'] = array('href' => true, 'xlink:href' => true);
$allowed['defs'] = array();
$allowed['symbol'] = array('id' => true, 'viewbox' => true);
}
return $allowed;
}, 10, 2);
This snippet explicitly whitelists common SVG elements and attributes. It also allows a <style> tag, which is normally stripped. Be deliberate here. Only whitelist what you actually need. If you allow <script> or event attributes, you reintroduce the XSS risk that WordPress is trying to prevent.

Why Your SVG Thumbnails Are Blank in the Media Library
Even after a successful upload, the media library often shows a generic placeholder instead of a preview of your SVG. This happens because WordPress generates thumbnails using PHP’s GD library or Imagick, both of which are raster image processors. They expect JPEG, PNG, or GIF input. When handed an SVG, they return nothing. The thumbnail generation fails silently, and WordPress falls back to a default icon.
The fix is to tell WordPress to use the SVG itself as the thumbnail. Since SVGs are vector and scale infinitely, using the original file as the preview is perfectly acceptable. You need to hook into the metadata generation process and supply the correct dimensions. Add this to your functions.php:
add_filter('wp_generate_attachment_metadata', function($metadata, $attachment_id) {
$mime = get_post_mime_type($attachment_id);
if ($mime === 'image/svg+xml') {
$file = get_attached_file($attachment_id);
$dimensions = svg_dimensions($file);
if ($dimensions) {
$metadata['width'] = $dimensions['width'];
$metadata['height'] = $dimensions['height'];
$metadata['file'] = basename($file);
}
}
return $metadata;
}, 10, 2);
function svg_dimensions($svg) {
$svg_content = file_get_contents($svg);
$xml = simplexml_load_string($svg_content);
if ($xml === false) return false;
$attributes = $xml->attributes();
$width = (string) $attributes->width;
$height = (string) $attributes->height;
if ($width && $height) {
return array('width' => intval($width), 'height' => intval($height));
}
$viewbox = (string) $attributes->viewBox;
if ($viewbox) {
$parts = explode(' ', $viewbox);
if (count($parts) === 4) {
return array('width' => intval($parts[2]), 'height' => intval($parts[3]));
}
}
return false;
}
This code reads the SVG file, extracts the width and height attributes or the viewBox, and sets the attachment metadata accordingly. WordPress then uses the original SVG as the thumbnail source. The preview in the media library becomes the actual vector graphic, rendered by the browser. No rasterization needed.
When the Media Library Grid Still Shows a Broken Icon
Sometimes the metadata fix works for the attachment details page but the grid view in the media library still shows a broken icon. This is because WordPress tries to generate intermediate image sizes for the grid thumbnails. Since GD and Imagick cannot process SVGs, those sub-sizes are never created. The grid falls back to a generic icon. To solve this, you need to filter the intermediate sizes and force WordPress to use the full-size SVG for all display contexts.
Add this filter to your functions.php:
add_filter('wp_prepare_attachment_for_js', function($response, $attachment, $meta) {
if ($response['mime'] === 'image/svg+xml') {
$response['sizes'] = array(
'full' => array(
'url' => $response['url'],
'width' => $response['width'],
'height' => $response['height'],
'orientation' => $response['orientation'],
),
);
}
return $response;
}, 10, 3);
This filter ensures that the media library grid and any image size requests always receive the full SVG. Since SVGs are resolution-independent, there is no downside to serving the original file at any requested dimension. The browser handles scaling natively.

Handling Inline SVG Display in the Block Editor
Uploading is only half the battle. When you insert an SVG into a post via the Image block, WordPress wraps it in an <img> tag. This works, but you lose the ability to style the SVG with CSS or manipulate it with JavaScript. For icons, logos, and illustrations that need to inherit text color or respond to hover states, inline SVG is the better approach. The block editor, however, does not provide a native inline SVG block.
You can work around this by using the Custom HTML block. Paste your SVG markup directly. But if you want the convenience of the media library, you can filter the image markup and replace the <img> tag with the actual SVG content when the file is an SVG. This is a more advanced technique that parses the attachment and injects the XML. Add this to your functions.php:
add_filter('wp_get_attachment_image_attributes', function($attr, $attachment) {
if (get_post_mime_type($attachment->ID) === 'image/svg+xml') {
$attr['data-svg'] = 'true';
}
return $attr;
}, 10, 2);
add_filter('the_content', function($content) {
return preg_replace_callback('/<img([^>]+)data-svg="true"([^>]*)>/', function($matches) {
$attributes = $matches[1] . $matches[2];
preg_match('/src="([^"]+)"/', $attributes, $src_match);
if (!empty($src_match[1])) {
$svg_content = file_get_contents($src_match[1]);
if ($svg_content !== false) {
return $svg_content;
}
}
return $matches[0];
}, $content);
});
This code marks SVG images with a data-svg attribute and then swaps the <img> tag for the raw SVG markup when the post content is rendered. The result is a fully inline SVG that you can style with your theme’s CSS. Be aware that this approach bypasses the kses sanitization on output, so you must trust the SVG files in your media library. If you allow other users to upload SVGs, combine this with the earlier sanitization extension to strip dangerous tags at upload time.
Why Your Server Configuration Might Still Block SVGs
Even with all the WordPress-level fixes, your web server can reject SVG uploads. Apache and Nginx sometimes have restrictive rules for XML-like files. If you see a 403 Forbidden error when accessing an uploaded SVG directly, check your server’s .htaccess or Nginx config. Some security plugins or hosting providers add rules that block files with xml in the MIME type or extension. Look for directives like:
<FilesMatch "\.(svg)$">
Order Deny,Allow
Deny from all
</FilesMatch>
If you find such a block, remove it or change it to Allow from all. On Nginx, check for location blocks that deny access to SVG files. This is less common but worth investigating if uploads work but direct viewing fails.
Sanitizing SVGs Before Upload: A Build Step
If you control the SVG source files, the best defense is a clean export. Most vector tools embed metadata, comments, and sometimes proprietary namespaces that bloat the file and trigger sanitization failures. Run your SVGs through a manual cleanup before uploading. Open the file in a code editor and remove:
- Comments (
<!-- ... -->) - Editor-specific namespaces (Illustrator’s
xmlns:i, Sketch’sxmlns:sketch) - Empty
<defs>or unused<symbol>elements - Embedded raster images (base64-encoded PNGs inside SVGs)
- Any
<script>or event handler attributes
This manual step reduces the file size and ensures the sanitizer has less work to do. It also makes your SVGs more portable across different systems. If you are working with a large number of SVGs, consider a command-line tool like SVGO to automate the cleanup. Run it as part of your build process before uploading assets to WordPress.
When You Should Still Use a Plugin
I am a builder. I prefer direct code over plugins when the logic is simple and maintainable. But there are cases where a plugin is the pragmatic choice. If you run a multi-author site where non-technical users upload SVGs regularly, the code snippets above require ongoing maintenance. A well-written SVG support plugin handles MIME detection, sanitization, thumbnails, and inline rendering in one package. It also provides a UI for managing allowed tags. The trade-off is adding another dependency to your stack. For a single-developer site or a tightly controlled theme, the code approach is lighter and more transparent.
If you are already debugging other WordPress quirks, you might find that SVG upload issues overlap with broader file permission or rewrite problems. I covered a related topic in What to Fix First When a New WordPress Site Says Nothing Found, which walks through the server and permalink checks that often underlie media library errors.
FAQ
Why does WordPress block SVG uploads by default?
SVGs are XML documents that can contain JavaScript, event handlers, and foreign objects. This makes them a vector for cross-site scripting (XSS) attacks. WordPress blocks them to protect site visitors and administrators from malicious code embedded in what appears to be a harmless image file.
Can I just add define('ALLOW_UNFILTERED_UPLOADS', true); to wp-config.php?
You can, but you should not. That constant disables all file type checks for administrators and is intended for multisite network admins who need to upload arbitrary files. It opens your site to serious security risks. The targeted filters described in this article are far safer.
Why do my SVG icons not change color when I hover over them?
If you insert SVGs as <img> tags, the browser treats them as static images. CSS hover effects on the parent element do not penetrate the image boundary. To make SVGs respond to CSS, you must inline them into the HTML. The inline replacement technique in this article solves that problem.
Will these fixes survive WordPress updates?
Yes, if you place the code in a child theme’s functions.php or a site-specific mu-plugin. Core updates do not overwrite theme or mu-plugin files. The filters used are stable WordPress hooks that have existed for many versions. Always test after a major update, but breakage is unlikely.