SVG Validator

Validates SVG as XML and flags anything executable. Never renders it.

Input
WaitingPaste a document to check it. Validation runs as you type.

Everything runs in this tab. Nothing you paste is uploaded, logged or sent anywhere. Open your network panel and check.

Paste an SVG above, or drop the file onto the editor, and it is checked as XML: every syntax error at once, each with its line, its column and the fix. SVG is an XML vocabulary, so an SVG that a build tool, an icon pipeline or a browser rejects is usually failing the XML rules rather than the SVG ones.

Reach for it when an export from Figma, Illustrator or Inkscape breaks a bundler, when an icon renders blank and you need to know whether the file parses at all, or when a user has uploaded an SVG and you want to read it before it goes near a browser.

The difference here is what the page refuses to do. Most SVG validators draw your file on screen beside the source, handing it to the same rendering engine that would run any script inside it. This page never renders it. Your markup stays text in an editor: every value the page displays is written with textContent or createElement, so no part of the file reaches innerHTML, and nothing is uploaded.

SVG is XML that browsers execute

An SVG can carry a <script> element, an onload or onmouseover attribute on any shape, an href beginning with javascript:, and a <foreignObject> holding arbitrary HTML. All of it is legal SVG and all of it is well-formed XML, which is why a syntax check alone can never tell you a file is safe.

When that content runs depends on how the file is used. Loaded through an <img> tag or a CSS background, an SVG executes no script and cannot reach the surrounding page. Inlined into your DOM, or opened at its own URL, it executes in the origin that served it. That second case turns an upload feature into stored cross-site scripting.

So this page checks the XML and declines to be the thing that runs the file. A <script> element is not reported as a syntax error, because it is not one. You get the markup as highlighted text with every well-formedness problem marked, and the audit code below to put an executable-content check into the build step where it belongs.

The SVG failures that are XML failures

These account for nearly every SVG a parser rejects, each reported with an exact position rather than a single "invalid SVG".

  • xlink:href used without xmlns:xlink="http://www.w3.org/1999/xlink" in scope. An unbound prefix is a hard error under Namespaces in XML, and it turns up constantly in fragments lifted out of a sprite sheet.
  • A <path>, <image>, <use> or <stop> left unclosed. HTML has void elements; XML does not, so <path d="..."> without a closing slash is an unclosed tag, and the error surfaces much later at whatever tag failed to match.
  • A raw ampersand in an xlink:href query string, which has to be written as &amp; inside an attribute value, or a literal < in a style attribute, which XML forbids there even though > is allowed.
  • The same attribute given twice on one element, which some export pipelines produce when transforms are merged badly.
  • The old Illustrator DOCTYPE pointing at the SVG 1.1 DTD. It is reported and never fetched, which is also why nothing here validates against that DTD.
  • An internal entity subset that expands into hundreds of megabytes. SVG is a popular carrier for that attack because upload forms accept it; the cost is computed from the declarations and the file is rejected in about two milliseconds.

What a well-formedness check cannot tell you

Being explicit about this is more useful than a green tick. Every file below is well-formed XML, passes here, and is still broken as SVG.

  • A root element with no xmlns="http://www.w3.org/2000/svg". Inline in HTML it usually still renders, because the HTML parser assumes the SVG namespace; through <img> or served as image/svg+xml it renders nothing. This is the commonest cause of "my SVG is blank".
  • viewbox instead of viewBox. XML attribute names are case sensitive and SVG defines only the camel-case spelling, so browsers ignore the misspelling and the icon scales wrongly rather than failing.
  • width and height set with no viewBox, which produces an image that will not scale, and a viewBox with a zero width or height, which produces nothing.
  • An xlink:href pointing at another file for a gradient, a filter or a <use> reference. It resolves while the file sits on your disk and breaks the moment the SVG is inlined or served from another origin, as does a <text> element set in a font that exists only on the designer's machine.

Before an uploaded SVG reaches a browser

If the file came from a user, treat reading it here as a first step and not the last. Matching on the word script is not a defence: payloads hide in base64 data URIs, in <set attributeName="onload">, in animate elements, and in namespace tricks a naive filter walks past.

The reliable options are a sanitiser that parses rather than greps, DOMPurify with its SVG profile in Node or the browser and enshrined/svg-sanitize in PHP, or rasterising to PNG server side. If you must serve the original, serve it from a separate origin with Content-Security-Policy set and reference it through <img> rather than inlining it. This page reports; it does not clean.

Auditing an SVG in code

Parse the file safely, then walk it looking for what a browser could execute. Every sample below disables entity resolution and network access first, because an SVG carrying a DOCTYPE is an XXE payload in exactly the way an ordinary XML document is.

// Parses the SVG and lists what a browser could execute. Nothing here
// inserts the markup into the page, which is what keeps it safe to run
// against a file you do not trust.
const EXECUTABLE = ['javascript:', 'vbscript:', 'data:text/html'];

function auditSvg(source) {
  const doc = new DOMParser().parseFromString(source, 'image/svg+xml');
  if (doc.querySelector('parsererror')) {
    return { wellFormed: false, findings: [] };
  }

  const findings = [];
  const walk = (node) => {
    const tag = node.localName.toLowerCase();
    if (tag === 'script' || tag === 'handler' || tag === 'foreignobject') {
      findings.push(node.nodeName + ' element');
    }
    for (const attr of node.attributes) {
      const value = attr.value.trim().toLowerCase();
      if (attr.name.toLowerCase().startsWith('on')) {
        findings.push(attr.name + ' event handler');
      }
      if (EXECUTABLE.some((scheme) => value.startsWith(scheme))) {
        findings.push('executable URI in ' + attr.name);
      }
    }
    for (const child of node.children) walk(child);
  };

  walk(doc.documentElement);
  return { wellFormed: true, findings };
}
# resolve_entities and no_network are the flags that matter: an SVG with a
# DOCTYPE is an XXE vector like any other XML document.
from lxml import etree

DANGEROUS = {'script', 'handler', 'foreignObject'}
EXECUTABLE = ('javascript:', 'vbscript:', 'data:text/html')

parser = etree.XMLParser(
    resolve_entities=False,
    no_network=True,
    load_dtd=False,
    huge_tree=False,
)
root = etree.fromstring(svg_bytes, parser)   # XMLSyntaxError if malformed

for el in root.iter():
    if not isinstance(el.tag, str):
        continue                             # comment or processing instruction
    if etree.QName(el).localname in DANGEROUS:
        print(el.sourceline, etree.QName(el).localname, 'element')
    for name, value in el.attrib.items():
        local = etree.QName(name).localname if name.startswith('{') else name
        if local.lower().startswith('on'):
            print(el.sourceline, name, 'event handler')
        if value.strip().lower().startswith(EXECUTABLE):
            print(el.sourceline, name, 'executable URI')
import javax.xml.XMLConstants;
import javax.xml.parsers.SAXParserFactory;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.helpers.DefaultHandler;
import java.util.Locale;
import java.util.Set;

SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
// Old Illustrator exports carry a DOCTYPE. Drop the next line only if you
// must accept them, and keep the two features above false either way.
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

final Set<String> dangerous = Set.of("script", "handler", "foreignObject");

factory.newSAXParser().parse(
    new InputSource(new java.io.StringReader(svg)),
    new DefaultHandler() {
        @Override
        public void startElement(String uri, String local, String qName, Attributes atts) {
            if (dangerous.contains(local)) {
                System.out.println("element: " + qName);
            }
            for (int i = 0; i < atts.getLength(); i++) {
                String name = atts.getQName(i);
                String value = atts.getValue(i).trim().toLowerCase(Locale.ROOT);
                if (name.toLowerCase(Locale.ROOT).startsWith("on")) {
                    System.out.println("event handler: " + name);
                }
                if (value.startsWith("javascript:") || value.startsWith("data:text/html")) {
                    System.out.println("executable URI in " + name);
                }
            }
        }
    });
using System;
using System.Xml;

var dangerous = new[] { "script", "handler", "foreignObject" };
var settings = new XmlReaderSettings
{
    DtdProcessing = DtdProcessing.Prohibit,  // no DOCTYPE, no entity expansion
    XmlResolver = null,                      // never fetch anything
    MaxCharactersFromEntities = 1024 * 1024,
};

using var reader = XmlReader.Create(new StringReader(svg), settings);
var lineInfo = (IXmlLineInfo)reader;

while (reader.Read())
{
    if (reader.NodeType != XmlNodeType.Element) continue;

    if (Array.IndexOf(dangerous, reader.LocalName) >= 0)
        Console.WriteLine($"line {lineInfo.LineNumber}: <{reader.Name}>");

    while (reader.MoveToNextAttribute())
    {
        var value = reader.Value.Trim().ToLowerInvariant();
        if (reader.LocalName.StartsWith("on", StringComparison.OrdinalIgnoreCase))
            Console.WriteLine($"line {lineInfo.LineNumber}: {reader.Name} handler");
        if (value.StartsWith("javascript:") || value.StartsWith("data:text/html"))
            Console.WriteLine($"line {lineInfo.LineNumber}: URI in {reader.Name}");
    }
    reader.MoveToElement();
}
<?php
// LIBXML_NONET blocks every external fetch, including a DOCTYPE's DTD.
libxml_use_internal_errors(true);

$doc = new DOMDocument();
if (!$doc->loadXML($svg, LIBXML_NONET)) {
    fwrite(STDERR, "not well-formed XML\n");
    exit(1);
}

$xpath = new DOMXPath($doc);
$xpath->registerNamespace('svg', 'http://www.w3.org/2000/svg');

foreach ($xpath->query('//svg:script | //svg:handler | //svg:foreignObject') as $node) {
    printf("%s element on line %d\n", $node->nodeName, $node->getLineNo());
}

foreach ($xpath->query('//@*') as $attr) {
    $name = strtolower($attr->nodeName);
    $value = strtolower(trim($attr->nodeValue));
    if (str_starts_with($name, 'on') || str_starts_with($value, 'javascript:')) {
        printf("%s on line %d\n", $attr->nodeName, $attr->getLineNo());
    }
}
// To clean rather than report, use enshrined/svg-sanitize.
# Well-formedness first. An SVG that fails here fails as XML.
xmllint --noout --nonet icon.svg

# A smoke test for executable content. This is a grep, not a sanitiser: it
# misses base64 payloads, <set attributeName="onload">, and obfuscation.
grep -niE 'script|foreignObject|on[a-z]+ *=|javascript:|data:text/html' icon.svg

# Check a directory of exports and keep going after each failure:
find . -name '*.svg' -print0 | xargs -0 -n1 xmllint --noout --nonet

# svgo is a minifier and does not remove script by default.
# For sanitising, use DOMPurify in Node with its SVG profile.

A deny list of tag and attribute names is a reporting tool, not a security boundary. Anything you actually serve to browsers should go through a sanitiser that parses the document and rebuilds it from an allow list, or be rasterised to PNG.

Common questions

Is my SVG uploaded, and is it displayed anywhere?

Neither. The parser is JavaScript running in this tab, so the file is never transmitted, and the page deliberately does not render it: no preview pane, no <img> pointed at your markup, no path by which it reaches innerHTML.

That matters more here than on the other tools, because an SVG carrying a script only becomes dangerous when something renders it: a validator that shows you a preview is running your untrusted file in its own origin. Your input is kept in this browser's localStorage so a refresh does not lose it, which stays on your machine, is removed by the Clear button, and is skipped for files over 300,000 characters.

Can an SVG file contain JavaScript?

Yes. SVG has a <script> element, event-handler attributes such as onload and onclick on any element, href values beginning with javascript:, and <foreignObject> for embedded HTML. None of it makes the file malformed; it is all part of the format.

Whether it runs depends on how the file is used. Through an <img> tag or a CSS background, script does not execute. Inlined into your page, or opened at its own URL, it executes with the privileges of the origin that served it, which is how an avatar upload becomes stored cross-site scripting. Sanitise before serving, and prefer <img> over inlining.

Why does my SVG pass this check but still show as blank?

Almost always a missing namespace. The root element needs xmlns="http://www.w3.org/2000/svg". Without it the file is still well-formed XML, and it often still renders when pasted inline into HTML because the HTML parser guesses the namespace, but loaded through <img> or served as image/svg+xml it produces nothing.

The other frequent causes are a viewBox misspelled as viewbox, which browsers ignore because XML attribute names are case sensitive, a viewBox with a zero width or height, and a reference to a gradient in a file that is no longer alongside it. None are XML errors, so no syntax checker reports them.

Does this validate SVG against the SVG specification?

No, and it says so rather than implying otherwise. It checks that the file is well-formed XML and that its namespace prefixes are declared. It does not check that <circle> has an r attribute, that a path's d data parses, or that a length is legal.

There is a DTD for SVG 1.1, and you can run a document against it on the DTD validator here, but no browser validates SVG against that DTD, SVG 2 defines none at all, and many files that render perfectly will fail it. Well-formedness plus a look at what the file executes is the check that matches how SVG is actually used.

What does "the prefix xlink is not bound to a namespace" mean?

It means the file uses xlink:href somewhere without an xmlns:xlink="http://www.w3.org/1999/xlink" declaration in scope. A prefix has no meaning until it is bound, so this is a fatal error rather than a warning, and every conforming parser rejects the file.

It usually appears after a fragment has been copied out of a sprite sheet, leaving the declaration behind on the original root. The fix is to add the declaration to the root element of the file you have. In SVG 2 the plain href attribute replaced xlink:href and needs no prefix, and current browsers support it, so for a file you control it is often simpler to drop the prefix.

Related tools

Background reading

Errors this fixes