Why Your SVG Uploads Break in WordPress and How to Fix Them Without Plugins

You drag a crisp SVG into the media library. The preview spins. Then WordPress throws the error: “Sorry, this file type is not permitted for security reasons.” Or worse—it uploads, but your icon renders as a broken thumbnail, or the file silently strips your carefully tuned viewBox. If you’ve been handed a design system that depends on scalable vector icons, this moment is a quiet rage trigger. I’ve hit it on three client builds this year alone. The good news: you don’t need a plugin to fix it. You need to understand what WordPress is actually rejecting and why—then patch the behavior at the right layer.

The Real Reason WordPress Blocks Your SVG

WordPress isn’t being stubborn. SVGs are XML documents, not flat images. An SVG file can carry inline scripts, event handlers, and external entity references—vectors for cross-site scripting (XSS) and XML entity expansion attacks. The core team’s default posture is to treat every SVG as potentially hostile. That’s why the allowed file type list in wp-includes/functions.php omits svg and svgz entirely. The MIME check fails, and the uploader rejects the file before it ever touches the media library.

But the security concern is only half the story. Even if you force the upload through, WordPress’s image subsystem doesn’t understand vector data. There’s no width/height extraction from the XML tree, no thumbnail generation, and no metadata parsing. The media modal shows a blank document icon because the attachment metadata array is empty. That’s a design gap, not a security feature—and it’s one you can bridge with a few lines of PHP that respect the security boundary.

What Actually Breaks: MIME Validation, Metadata, and Rendering

Three distinct failures stack up when you try to upload an SVG without preparation. First, the wp_check_filetype_and_ext filter runs a MIME-type lookup against a hardcoded map. SVGs often arrive with image/svg+xml, which isn’t in the map. Second, even if the file passes the type gate, the attachment metadata generator—wp_generate_attachment_metadata—returns nothing useful for XML-based files. No width, no height, no thumbnail crop data. Third, the media library preview relies on that metadata to display a thumbnail. Without it, you get the broken document icon. The file is there, but WordPress doesn’t know how to show it.

Step 1: Allow the MIME Type Without Opening the Floodgates

The cleanest entry point is the upload_mimes filter. It lets you add svg to the allowed list before the type checker runs. But don’t stop there. Pair it with a server-side sanitization step that strips scripts, event handlers, and foreign objects from the XML before the file lands in the uploads directory. This keeps the security boundary intact while granting designers the format they need.

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

That snippet alone gets you past the upload gate. But it doesn’t sanitize the file, and it doesn’t fix the preview. For a production site, you need a second filter that intercepts the file during the upload prefilter phase. Hook into wp_handle_upload_prefilter and run the SVG through DOMDocument, stripping <script> tags, onload attributes, and any namespace you don’t explicitly trust. This is the same approach used by well-audited SVG sanitizer libraries, but you can implement a minimal version that covers 95% of real-world icon files without pulling in a dependency.

add_filter( 'wp_handle_upload_prefilter', function( $file ) {
    if ( 'image/svg+xml' !== $file['type'] ) {
        return $file;
    }
    $svg = file_get_contents( $file['tmp_name'] );
    $dom = new DOMDocument();
    $dom->loadXML( $svg, LIBXML_NOENT | LIBXML_NSCLEAN );
    $allowed_elements = [ 'svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'defs', 'use', 'title', 'desc' ];
    $allowed_attrs    = [ 'viewBox', 'xmlns', 'd', 'fill', 'stroke', 'stroke-width', 'cx', 'cy', 'r', 'x', 'y', 'width', 'height', 'transform', 'id', 'class' ];
    $xpath = new DOMXPath( $dom );
    foreach ( $xpath->query( '//*' ) as $node ) {
        if ( ! in_array( $node->nodeName, $allowed_elements, true ) ) {
            $node->parentNode->removeChild( $node );
            continue;
        }
        for ( $i = $node->attributes->length - 1; $i >= 0; $i-- ) {
            $attr = $node->attributes->item( $i );
            if ( ! in_array( $attr->name, $allowed_attrs, true ) ) {
                $node->removeAttribute( $attr->name );
            }
        }
    }
    file_put_contents( $file['tmp_name'], $dom->saveXML() );
    return $file;
});

This filter runs before the file is moved to the uploads directory. It strips anything that isn’t a structural or presentational SVG element. If your icons come from a trusted design pipeline—Figma exports, Illustrator artboards with “presentation attributes” enabled—this sanitizer won’t touch the visual output. It only removes the dangerous cruft that some editors inject by default.

Step 2: Generate Attachment Metadata So Thumbnails Work

Once the SVG is stored, WordPress tries to build metadata by calling wp_generate_attachment_metadata. For raster images, this extracts dimensions and creates sub-sizes. For SVGs, it returns an empty array because the function relies on PHP’s getimagesize(), which doesn’t parse XML. The fix is to hook into wp_generate_attachment_metadata and supply the missing width and height directly from the SVG’s viewBox or width/height attributes.

add_filter( 'wp_generate_attachment_metadata', function( $metadata, $attachment_id ) {
    $mime = get_post_mime_type( $attachment_id );
    if ( 'image/svg+xml' !== $mime ) {
        return $metadata;
    }
    $file = get_attached_file( $attachment_id );
    $svg  = simplexml_load_file( $file );
    $attr = $svg->attributes();
    $viewBox = explode( ' ', (string) $attr->viewBox );
    if ( count( $viewBox ) === 4 ) {
        $width  = intval( $viewBox[2] );
        $height = intval( $viewBox[3] );
    } elseif ( isset( $attr->width, $attr->height ) ) {
        $width  = intval( $attr->width );
        $height = intval( $attr->height );
    } else {
        return $metadata;
    }
    $metadata['width']  = $width;
    $metadata['height'] = $height;
    $metadata['file']   = basename( $file );
    return $metadata;
}, 10, 2 );

With this in place, the media library reads the dimensions and renders a proper preview. The SVG still won’t generate cropped thumbnails—WordPress’s image editor can’t resize vectors—but the full-size preview becomes usable. For most icon and illustration workflows, that’s enough.

Step 3: Fix the Media Library Preview Display

Even with correct metadata, the media library grid uses wp_get_attachment_image, which wraps the file in an <img> tag. Browsers render SVGs fine in <img> elements, but WordPress’s responsive image logic adds srcset and sizes attributes that reference non-existent thumbnail files. The result is a broken image or a fallback to the generic document icon. You can intercept this with the wp_get_attachment_image_src filter, returning only the full-size URL for SVGs.

add_filter( 'wp_get_attachment_image_src', function( $image, $attachment_id, $size, $icon ) {
    if ( 'image/svg+xml' === get_post_mime_type( $attachment_id ) ) {
        $file = get_attached_file( $attachment_id );
        if ( $file ) {
            $image[0] = wp_get_attachment_url( $attachment_id );
            $image[1] = null;
            $image[2] = null;
        }
    }
    return $image;
}, 10, 4 );

This filter strips the width and height from the image downsize logic, forcing the browser to render the SVG at its natural aspect ratio. If you need the SVG to scale within a specific container, handle that with CSS max-width: 100%; height: auto; on the front end—don’t rely on WordPress to generate raster fallbacks that don’t exist.

When You Should Still Avoid SVGs in the Library

This approach works for design-forward sites where the SVG is the final asset: logos, icons, spot illustrations. It’s not a good fit if you expect non-technical content editors to upload arbitrary SVGs from the web. The sanitizer above is minimal; it won’t catch every edge case in a user-submitted file. If your workflow involves untrusted SVG sources, you need a full XML security library—and at that point, a vetted plugin that wraps such a library is the safer architectural choice. But for a controlled build pipeline where you or your designers own the source files, the code above keeps your functions.php lean and your dependency tree short.

Putting It All Together: A Single mu-plugin File

I prefer to bundle SVG support as a must-use plugin rather than scattering filters across a theme’s functions.php. This keeps the logic portable and independent of theme switches. Create a file at wp-content/mu-plugins/svg-upload.php with all three filters. The mu-plugin directory loads automatically, so you don’t need to activate anything. Here’s the complete file structure:

<?php
/**
 * Plugin Name: SVG Upload Support (No Plugin)
 * Description: Enables sanitized SVG uploads and fixes media preview without a third-party plugin.
 * Author: Jonas Venn
 */

// 1. Allow SVG MIME type
add_filter( 'upload_mimes', function( $mimes ) {
    $mimes['svg'] = 'image/svg+xml';
    return $mimes;
});

// 2. Sanitize SVG on upload
add_filter( 'wp_handle_upload_prefilter', function( $file ) {
    if ( 'image/svg+xml' !== $file['type'] ) {
        return $file;
    }
    $svg = file_get_contents( $file['tmp_name'] );
    $dom = new DOMDocument();
    $dom->loadXML( $svg, LIBXML_NOENT | LIBXML_NSCLEAN );
    $allowed_elements = [ 'svg', 'g', 'path', 'circle', 'rect', 'line', 'polyline', 'polygon', 'ellipse', 'defs', 'use', 'title', 'desc' ];
    $allowed_attrs   = [ 'viewBox', 'xmlns', 'd', 'fill', 'stroke', 'stroke-width', 'cx', 'cy', 'r', 'x', 'y', 'width', 'height', 'transform', 'id', 'class' ];
    $xpath = new DOMXPath( $dom );
    foreach ( $xpath->query( '//*' ) as $node ) {
        if ( ! in_array( $node->nodeName, $allowed_elements, true ) ) {
            $node->parentNode->removeChild( $node );
            continue;
        }
        for ( $i = $node->attributes->length - 1; $i >= 0; $i-- ) {
            $attr = $node->attributes->item( $i );
            if ( ! in_array( $attr->name, $allowed_attrs, true ) ) {
                $node->removeAttribute( $attr->name );
            }
        }
    }
    file_put_contents( $file['tmp_name'], $dom->saveXML() );
    return $file;
});

// 3. Generate attachment metadata for preview dimensions
add_filter( 'wp_generate_attachment_metadata', function( $metadata, $attachment_id ) {
    if ( 'image/svg+xml' !== get_post_mime_type( $attachment_id ) ) {
        return $metadata;
    }
    $file = get_attached_file( $attachment_id );
    $svg  = simplexml_load_file( $file );
    $attr = $svg->attributes();
    $viewBox = explode( ' ', (string) $attr->viewBox );
    if ( count( $viewBox ) === 4 ) {
        $width  = intval( $viewBox[2] );
        $height = intval( $viewBox[3] );
    } elseif ( isset( $attr->width, $attr->height ) ) {
        $width  = intval( $attr->width );
        $height = intval( $attr->height );
    } else {
        return $metadata;
    }
    $metadata['width']  = $width;
    $metadata['height'] = $height;
    $metadata['file']   = basename( $file );
    return $metadata;
}, 10, 2 );

// 4. Prevent srcset generation for SVGs in media views
add_filter( 'wp_get_attachment_image_src', function( $image, $attachment_id, $size, $icon ) {
    if ( 'image/svg+xml' === get_post_mime_type( $attachment_id ) ) {
        $file = get_attached_file( $attachment_id );
        if ( $file ) {
            $image[0] = wp_get_attachment_url( $attachment_id );
            $image[1] = null;
            $image[2] = null;
        }
    }
    return $image;
}, 10, 4 );

Drop this file into wp-content/mu-plugins/ and your SVG uploads will work. No plugin updates to track, no license keys, no compatibility breaks when Gutenberg shifts its media handling again. You own the logic.

Testing the Pipeline with Real SVGs

After adding the mu-plugin, test with a few SVG variants. Start with a minimal icon exported from Figma using the “SVG” preset with “presentation attributes” styling. The file should upload, display a thumbnail in the media library, and render correctly in a post or page. Next, test an SVG that contains inline styles and a <script> tag—something you might accidentally get from a stock icon site. The sanitizer should strip the script and any style attributes not in the allowed list, while preserving the visual output. Finally, test an SVG with explicit width and height attributes but no viewBox. The metadata filter should fall back to those values and still produce a working preview.

Developer reviewing SVG code on a laptop screen

If the thumbnail still shows a document icon, check that your SVG has either a valid viewBox or explicit width/height attributes. The metadata filter depends on at least one of these being present. SVGs without dimensional data will fall through and return an empty metadata array, which triggers the fallback icon.

Handling the Frontend Display

Once the file is in the library, you’ll likely use it in blocks, shortcodes, or template code. The native Image block works, but it wraps the SVG in an <img> tag. That’s fine for most cases, but you lose the ability to style the SVG’s internal elements with CSS. If you need dynamic fill colors or hover states on individual paths, inline the SVG directly into your template using file_get_contents and echo it inside a container div. This gives you full CSS control over the vector DOM. Just make sure you’re pulling from a trusted source—your own media library, after sanitization.

For block-based themes, you can register a custom block that pulls the attachment URL and inlines the SVG markup server-side. That’s a larger build, but the foundation you’ve laid with the mu-plugin makes it possible. The attachment is clean, the metadata is present, and the file is accessible via wp_get_attachment_url.

Why This Beats Installing a Plugin

There are several well-known SVG support plugins. They work. But they also add admin menus, settings pages, and sometimes their own sanitizer libraries that duplicate what PHP’s DOMDocument already does. On a site where you’re already managing a custom theme and a handful of mu-plugins, adding another plugin for a single file type is overhead. The code above is 60 lines, fully auditable, and does exactly what you need—nothing more. When WordPress core eventually adds native SVG handling (it’s been discussed for years), you can remove the mu-plugin without untangling a plugin’s database options.

Clean code editor with PHP syntax highlighting

This approach also aligns with how I handle other WordPress quirks. When a new site returns “Nothing Found” for every permalink, I don’t install a plugin—I check the rewrite rules and flush them. The same builderly mindset applies here: understand the mechanism, then write the smallest possible fix. If you’re dealing with that permalink issue, I’ve written a separate guide on what to fix first when a new WordPress site says nothing found that follows the same philosophy.

Security Notes for Production

The sanitizer above uses a whitelist approach: it removes anything not explicitly allowed. This is safer than blacklisting known-bad elements because it protects against future SVG attack vectors that haven’t been cataloged yet. The LIBXML_NOENT flag prevents entity expansion attacks. The LIBXML_NSCLEAN flag removes redundant namespace declarations. Together, they harden the XML parsing step.

If your site allows user registrations with upload capabilities, consider adding a capability check before the MIME filter runs. You can restrict SVG uploads to administrators and editors by wrapping the upload_mimes filter in a current_user_can( 'edit_posts' ) conditional. For most brochure sites and portfolios, this isn’t necessary—the uploader is already gated behind authentication.

When the Fix Doesn’t Stick: Debugging Checklist

If you’ve added the mu-plugin and SVGs still fail, work through this list:

  • File encoding: The SVG must be valid XML. Open it in a text editor and check for BOM characters or HTML entities that break the parser.
  • PHP DOM extension: The sanitizer requires DOMDocument and DOMXPath. These are enabled by default in most PHP builds, but some minimal hosting setups disable them. Verify with phpinfo().
  • mu-plugin loading order: Must-use plugins load before regular plugins and themes. If another plugin is filtering upload_mimes with a higher priority and removing your addition, you’ll need to adjust the priority or find the conflict.
  • File permissions: The uploads directory must be writable. If the file moves but the metadata filter can’t read it back, check ownership and permissions.
  • Caching layer: If you use a CDN or object cache, flush it after adding the mu-plugin. Stale MIME-type lists can survive in opcode caches.

Server rack with blinking lights in a data center

Extending the Approach for SVGZ and Inline Use

Compressed SVG files (.svgz) are gzipped SVGs. To support them, add svgz to the MIME map with image/svg+xml and decompress the file before sanitization. The wp_handle_upload_prefilter can check the extension and run gzdecode before passing the XML to DOMDocument. For inline SVG use in templates, you can skip the upload flow entirely and store SVG strings as theme options or custom post meta—but then you lose the media library’s organizational benefits. The mu-plugin approach keeps the asset in the library where it belongs.

FAQ

Is it safe to allow SVG uploads on a client site?

Yes, if you sanitize the file on upload and restrict upload capabilities to trusted roles. The sanitizer in this guide strips scripts, event handlers, and foreign objects. Combined with WordPress’s existing authentication layer, this is safe for sites where only administrators and editors can upload files.

Why does my SVG thumbnail still show a broken icon after adding the code?

The most common cause is a missing viewBox or width/height attribute in the SVG file. The metadata filter needs at least one of these to extract dimensions. Open the SVG in a text editor and confirm the root <svg> element has a viewBox (e.g., viewBox="0 0 24 24") or explicit width and height.

Can I use this method with the block editor’s Image block?

Yes. The Image block will display the SVG using an <img> tag. The wp_get_attachment_image_src filter prevents broken srcset references. If you need to style the SVG’s internal paths with CSS, you’ll need to inline the SVG markup instead of using the Image block.

What happens when WordPress core eventually supports SVGs?

You can remove the mu-plugin. The filters are additive; they won’t conflict with native support because they only act on the SVG MIME type. If core adds its own sanitization and metadata handling, your filters may become redundant, but they won’t break anything.

SVG uploads don’t have to be a fragile workaround. With a single mu-plugin file, you get sanitized uploads, working previews, and a media library that treats vectors as first-class assets. The code is yours to audit, extend, and remove when it’s no longer needed. That’s the builder’s way.