SVG-Validator

Prüft SVG als XML und meldet Ausführbares. Ohne es zu rendern.

Eingabe
WartetFügen Sie ein Dokument ein. Die Prüfung läuft während der Eingabe.

Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.

Fügen Sie oben ein SVG ein oder ziehen Sie die Datei auf den Editor: Sie wird als XML geprüft, jeder Syntaxfehler auf einmal, jeder mit Zeile, Spalte und der Korrektur. SVG ist ein XML-Vokabular, deshalb verstößt ein SVG, das ein Build-Werkzeug, eine Icon-Pipeline oder ein Browser zurückweist, meist gegen die XML-Regeln und nicht gegen die von SVG.

Greifen Sie danach, wenn ein Export aus Figma, Illustrator oder Inkscape einen Bundler zerlegt, wenn ein Icon leer bleibt und Sie wissen müssen, ob die Datei überhaupt parst, oder wenn jemand ein SVG hochgeladen hat und Sie es lesen wollen, bevor es in die Nähe eines Browsers kommt.

Der Unterschied liegt hier darin, was die Seite sich weigert zu tun. Die meisten SVG-Validatoren zeichnen Ihre Datei neben dem Quelltext auf den Bildschirm und übergeben sie damit derselben Render-Engine, die auch jedes Skript darin ausführen würde. Diese Seite rendert sie nie. Ihr Markup bleibt Text in einem Editor: Jeder Wert, den die Seite anzeigt, wird mit textContent oder createElement geschrieben, also erreicht kein Teil der Datei innerHTML, und nichts wird hochgeladen.

SVG ist XML, das Browser ausführen

Ein SVG kann ein <script>-Element tragen, ein onload- oder onmouseover-Attribut an jeder Form, ein href, das mit javascript: beginnt, und ein <foreignObject> mit beliebigem HTML darin. All das ist zulässiges SVG und all das ist wohlgeformtes XML, und darum kann eine Syntaxprüfung allein Ihnen nie sagen, dass eine Datei sicher ist.

Wann dieser Inhalt läuft, hängt davon ab, wie die Datei benutzt wird. Über ein <img>-Tag oder einen CSS-Hintergrund geladen, führt ein SVG kein Skript aus und kommt an die umgebende Seite nicht heran. In Ihr DOM eingebettet oder unter seiner eigenen URL geöffnet, läuft es in dem Origin, der es ausgeliefert hat. Der zweite Fall macht aus einer Upload-Funktion gespeichertes Cross-Site-Scripting.

Also prüft diese Seite das XML und lehnt es ab, das Ding zu sein, das die Datei ausführt. Ein <script>-Element wird nicht als Syntaxfehler gemeldet, weil es keiner ist. Sie bekommen das Markup als hervorgehobenen Text mit jedem Wohlgeformtheitsproblem markiert, und den Auditcode weiter unten, um eine Prüfung auf ausführbaren Inhalt dorthin zu setzen, wo sie hingehört: in den Build-Schritt.

Die SVG-Fehler, die XML-Fehler sind

Auf sie geht fast jedes SVG zurück, das ein Parser zurückweist, und jeder wird mit genauer Position gemeldet statt mit einem einzigen „ungültiges SVG“.

  • xlink:href benutzt, ohne dass xmlns:xlink="http://www.w3.org/1999/xlink" im Gültigkeitsbereich liegt. Ein ungebundenes Präfix ist nach Namespaces in XML ein harter Fehler, und er taucht ständig in Fragmenten auf, die aus einem Sprite-Sheet herausgelöst wurden.
  • Ein <path>, <image>, <use> oder <stop>, das nicht geschlossen wurde. HTML hat leere Elemente; XML nicht, also ist <path d="..."> ohne schließenden Schrägstrich ein offenes Tag, und der Fehler tritt viel später zutage, bei dem Tag, das nicht mehr passt.
  • Ein nacktes Kaufmanns-Und in einem xlink:href-Query-String, das in einem Attributwert als &amp; geschrieben werden muss, oder ein wörtliches < in einem style-Attribut, das XML dort verbietet, obwohl > erlaubt ist.
  • Dasselbe Attribut zweimal an einem Element, was manche Export-Pipelines erzeugen, wenn Transformationen schlecht zusammengeführt werden.
  • Das alte Illustrator-DOCTYPE, das auf die SVG-1.1-DTD zeigt. Es wird gemeldet und nie geladen, was auch der Grund ist, warum hier nichts gegen diese DTD validiert wird.
  • Ein internes Entity-Subset, das sich zu Hunderten von Megabyte aufbläht. SVG ist ein beliebter Träger für diesen Angriff, weil Upload-Formulare es annehmen; die Kosten werden aus den Deklarationen berechnet und die Datei wird in etwa zwei Millisekunden abgelehnt.

Was eine Wohlgeformtheitsprüfung Ihnen nicht sagen kann

Das klar zu benennen nützt mehr als ein grüner Haken. Jede Datei unten ist wohlgeformtes XML, kommt hier durch, und ist als SVG trotzdem kaputt.

  • Ein Wurzelelement ohne xmlns="http://www.w3.org/2000/svg". Inline in HTML wird es meist trotzdem gezeichnet, weil der HTML-Parser den SVG-Namensraum annimmt; über <img> oder als image/svg+xml ausgeliefert zeichnet es nichts. Das ist die häufigste Ursache für „mein SVG ist leer“.
  • viewbox statt viewBox. XML-Attributnamen unterscheiden Groß- und Kleinschreibung, und SVG definiert nur die Camel-Case-Schreibung, also ignorieren Browser den Schreibfehler, und das Icon skaliert falsch, statt zu scheitern.
  • width und height gesetzt ohne viewBox, was ein Bild ergibt, das nicht skaliert, und ein viewBox mit Breite oder Höhe null, das gar nichts ergibt.
  • Ein xlink:href, das für einen Verlauf, einen Filter oder eine <use>-Referenz auf eine andere Datei zeigt. Es löst auf, solange die Datei auf Ihrer Platte liegt, und bricht in dem Moment, in dem das SVG eingebettet oder von einem anderen Origin ausgeliefert wird – genau wie ein <text>-Element, das in einer Schrift gesetzt ist, die es nur auf dem Rechner des Gestalters gibt.

Bevor ein hochgeladenes SVG einen Browser erreicht

Kam die Datei von einem Nutzer, behandeln Sie das Lesen hier als ersten Schritt und nicht als letzten. Auf das Wort script zu prüfen ist keine Verteidigung: Nutzlasten verstecken sich in base64-Data-URIs, in <set attributeName="onload">, in animate-Elementen und in Namensraumtricks, an denen ein naiver Filter vorbeiläuft.

Die verlässlichen Optionen sind ein Sanitizer, der parst statt zu greppen – DOMPurify mit seinem SVG-Profil in Node oder im Browser und enshrined/svg-sanitize in PHP – oder serverseitiges Rastern nach PNG. Müssen Sie das Original ausliefern, liefern Sie es von einem eigenen Origin mit gesetzter Content-Security-Policy aus und referenzieren Sie es über <img>, statt es einzubetten. Diese Seite meldet; sie säubert nicht.

Ein SVG im Code prüfen

Parsen Sie die Datei sicher und gehen Sie sie dann nach dem durch, was ein Browser ausführen könnte. Jedes Beispiel unten schaltet zuerst Entity-Auflösung und Netzzugriff ab, denn ein SVG mit einem DOCTYPE ist genauso eine XXE-Nutzlast wie ein gewöhnliches XML-Dokument.

// 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.

Eine Sperrliste von Tag- und Attributnamen ist ein Meldewerkzeug, keine Sicherheitsgrenze. Alles, was Sie tatsächlich an Browser ausliefern, sollte durch einen Sanitizer gehen, der das Dokument parst und aus einer Erlaubnisliste neu aufbaut, oder nach PNG gerastert werden.

Häufige Fragen

Wird mein SVG hochgeladen, und wird es irgendwo angezeigt?

Weder noch. Der Parser ist JavaScript, das in diesem Tab läuft, die Datei wird also nie übertragen, und die Seite rendert sie bewusst nicht: kein Vorschaubereich, kein <img>, das auf Ihr Markup zeigt, kein Pfad, auf dem sie innerHTML erreicht.

Das zählt hier mehr als bei den anderen Werkzeugen, denn ein SVG mit einem Skript wird erst gefährlich, wenn etwas es rendert: Ein Validator, der Ihnen eine Vorschau zeigt, führt Ihre nicht vertrauenswürdige Datei in seinem eigenen Origin aus. Ihre Eingabe wird im localStorage dieses Browsers gehalten, damit ein Neuladen sie nicht verliert; das bleibt auf Ihrem Rechner, wird vom Leeren-Knopf entfernt und bei Dateien über 300.000 Zeichen übersprungen.

Kann eine SVG-Datei JavaScript enthalten?

Ja. SVG hat ein <script>-Element, Event-Handler-Attribute wie onload und onclick an jedem Element, href-Werte, die mit javascript: beginnen, und <foreignObject> für eingebettetes HTML. Nichts davon macht die Datei fehlerhaft; es gehört alles zum Format.

Ob es läuft, hängt davon ab, wie die Datei benutzt wird. Über ein <img>-Tag oder einen CSS-Hintergrund wird kein Skript ausgeführt. In Ihre Seite eingebettet oder unter seiner eigenen URL geöffnet, läuft es mit den Rechten des Origins, der es ausgeliefert hat – so wird aus einem Avatar-Upload gespeichertes Cross-Site-Scripting. Säubern Sie vor dem Ausliefern, und bevorzugen Sie <img> gegenüber dem Einbetten.

Warum besteht mein SVG diese Prüfung und bleibt trotzdem leer?

Fast immer ein fehlender Namensraum. Das Wurzelelement braucht xmlns="http://www.w3.org/2000/svg". Ohne ihn ist die Datei weiterhin wohlgeformtes XML, und inline in HTML eingefügt wird sie oft trotzdem gezeichnet, weil der HTML-Parser den Namensraum errät; über <img> geladen oder als image/svg+xml ausgeliefert ergibt sie nichts.

Die anderen häufigen Ursachen sind ein als viewbox geschriebenes viewBox, das Browser ignorieren, weil XML-Attributnamen Groß- und Kleinschreibung unterscheiden, ein viewBox mit Breite oder Höhe null, und ein Verweis auf einen Verlauf in einer Datei, die nicht mehr danebenliegt. Keines davon ist ein XML-Fehler, also meldet sie kein Syntaxprüfer.

Validiert das SVG gegen die SVG-Spezifikation?

Nein, und das wird gesagt statt anderes anzudeuten. Geprüft wird, dass die Datei wohlgeformtes XML ist und dass ihre Namensraumpräfixe deklariert sind. Nicht geprüft wird, ob <circle> ein r-Attribut hat, ob die d-Daten eines path parsen oder ob eine Länge zulässig ist.

Es gibt eine DTD für SVG 1.1, und Sie können ein Dokument hier im DTD-Validator dagegen laufen lassen, aber kein Browser validiert SVG gegen diese DTD, SVG 2 definiert gar keine, und viele Dateien, die einwandfrei gezeichnet werden, fallen durch. Wohlgeformtheit plus ein Blick darauf, was die Datei ausführt, ist die Prüfung, die dazu passt, wie SVG tatsächlich benutzt wird.

Was bedeutet „das Präfix xlink ist an keinen Namensraum gebunden“?

Es bedeutet, dass die Datei irgendwo xlink:href benutzt, ohne dass eine Deklaration xmlns:xlink="http://www.w3.org/1999/xlink" im Gültigkeitsbereich liegt. Ein Präfix hat keine Bedeutung, bevor es gebunden ist, also ist das ein fataler Fehler und keine Warnung, und jeder konforme Parser weist die Datei zurück.

Es erscheint meist, nachdem ein Fragment aus einem Sprite-Sheet kopiert wurde und die Deklaration an der ursprünglichen Wurzel zurückblieb. Die Korrektur ist, die Deklaration an das Wurzelelement der Datei zu setzen, die Sie haben. In SVG 2 hat das schlichte href-Attribut xlink:href abgelöst und braucht kein Präfix, und aktuelle Browser unterstützen es, für eine Datei, die Ihnen gehört, ist es also oft einfacher, das Präfix fallen zu lassen.

Verwandte Werkzeuge

Zum Weiterlesen

Fehler, die das behebt