Validateur SVG

Valide le SVG comme du XML et signale l’exécutable. Sans le rendre.

Entrée
En attenteCollez un document pour le vérifier. La validation se fait à la frappe.

Tout s'exécute dans cet onglet. Rien de ce que vous collez n'est envoyé, journalisé ni transmis où que ce soit. Ouvrez votre panneau réseau et vérifiez.

Collez un SVG ci-dessus, ou déposez le fichier sur l’éditeur : il est contrôlé en tant que XML, toutes les erreurs de syntaxe d’un coup, chacune avec sa ligne, sa colonne et la correction. SVG est un vocabulaire XML, donc un SVG qu’un outil de build, une chaîne d’icônes ou un navigateur rejette enfreint le plus souvent les règles d’XML plutôt que celles de SVG.

Servez-vous-en quand un export de Figma, Illustrator ou Inkscape casse un bundler, quand une icône s’affiche vide et que vous voulez savoir si le fichier s’analyse seulement, ou quand un utilisateur a téléversé un SVG et que vous voulez le lire avant qu’il n’approche d’un navigateur.

La différence ici tient à ce que la page refuse de faire. La plupart des validateurs SVG dessinent votre fichier à l’écran à côté de la source, en le confiant au moteur de rendu même qui exécuterait tout script qu’il contient. Cette page ne le rend jamais. Votre balisage reste du texte dans un éditeur : chaque valeur affichée par la page est écrite avec textContent ou createElement, donc aucune partie du fichier n’atteint innerHTML, et rien n’est téléversé.

SVG, c’est du XML que les navigateurs exécutent

Un SVG peut porter un élément <script>, un attribut onload ou onmouseover sur n’importe quelle forme, un href commençant par javascript:, et un <foreignObject> contenant du HTML arbitraire. Tout cela est du SVG licite et tout cela est du XML bien formé : c’est pourquoi un contrôle de syntaxe seul ne peut jamais vous dire qu’un fichier est sûr.

Le moment où ce contenu s’exécute dépend de l’usage du fichier. Chargé par une balise <img> ou un fond CSS, un SVG n’exécute aucun script et ne peut pas atteindre la page qui l’entoure. Intégré dans votre DOM, ou ouvert à sa propre URL, il s’exécute dans l’origine qui l’a servi. Ce second cas transforme une fonction de téléversement en cross-site scripting stocké.

Cette page contrôle donc le XML et refuse d’être ce qui exécute le fichier. Un élément <script> n’est pas signalé comme erreur de syntaxe, parce que ce n’en est pas une. Vous obtenez le balisage sous forme de texte coloré avec chaque problème de bonne formation marqué, et le code d’audit plus bas pour placer un contrôle du contenu exécutable là où il a sa place : dans l’étape de build.

Les échecs SVG qui sont des échecs XML

Ils expliquent presque tous les SVG qu’un analyseur rejette, chacun signalé avec une position exacte plutôt qu’un simple « SVG invalide ».

  • xlink:href utilisé sans xmlns:xlink="http://www.w3.org/1999/xlink" dans la portée. Un préfixe non lié est une erreur fatale selon Namespaces in XML, et cela revient sans cesse dans les fragments extraits d’une feuille de sprites.
  • Un <path>, <image>, <use> ou <stop> laissé non fermé. HTML a des éléments vides ; XML n’en a pas, donc <path d="..."> sans barre oblique fermante est une balise non fermée, et l’erreur ne remonte que bien plus loin, sur la balise qui n’a pas pu correspondre.
  • Une esperluette nue dans une chaîne de requête d’xlink:href, qui doit s’écrire &amp; dans une valeur d’attribut, ou un < littéral dans un attribut style, que XML y interdit alors même que > est permis.
  • Le même attribut donné deux fois sur un élément, ce que produisent certaines chaînes d’export quand les transformations sont mal fusionnées.
  • Le vieux DOCTYPE d’Illustrator pointant vers la DTD SVG 1.1. Il est signalé et jamais récupéré, ce qui explique aussi qu’ici rien ne soit validé avec cette DTD.
  • Un sous-ensemble d’entités internes qui se déploie en centaines de mégaoctets. SVG est un vecteur prisé pour cette attaque parce que les formulaires de téléversement l’acceptent ; le coût est calculé à partir des déclarations et le fichier est rejeté en deux millisecondes environ.

Ce qu’un contrôle de bonne formation ne peut pas vous dire

Le dire explicitement est plus utile qu’une coche verte. Tous les fichiers ci-dessous sont du XML bien formé, passent ici, et restent cassés en tant que SVG.

  • Un élément racine sans xmlns="http://www.w3.org/2000/svg". Intégré dans du HTML, il s’affiche généralement quand même, car l’analyseur HTML suppose l’espace de noms SVG ; via <img> ou servi en image/svg+xml, il n’affiche rien. C’est la cause la plus fréquente du « mon SVG est vide ».
  • viewbox au lieu de viewBox. Les noms d’attributs XML sont sensibles à la casse et SVG ne définit que l’orthographe en casse chameau : les navigateurs ignorent donc la faute, et l’icône se met à l’échelle de travers au lieu d’échouer.
  • width et height définis sans viewBox, ce qui donne une image qui ne se met pas à l’échelle, et un viewBox de largeur ou de hauteur nulle, qui ne donne rien.
  • Un xlink:href pointant vers un autre fichier pour un dégradé, un filtre ou une référence <use>. Il se résout tant que le fichier est sur votre disque et casse dès que le SVG est intégré ou servi depuis une autre origine, tout comme un élément <text> composé dans une fonte qui n’existe que sur la machine du graphiste.

Avant qu’un SVG téléversé n’atteigne un navigateur

Si le fichier vient d’un utilisateur, considérez sa lecture ici comme une première étape et non la dernière. Chercher le mot script n’est pas une défense : les charges utiles se cachent dans des URI de données en base64, dans <set attributeName="onload">, dans des éléments animate, et dans des ruses d’espaces de noms qu’un filtre naïf laisse passer.

Les options fiables sont un assainisseur qui analyse au lieu de faire du grep — DOMPurify avec son profil SVG en Node ou dans le navigateur, et enshrined/svg-sanitize en PHP — ou une rastérisation en PNG côté serveur. Si vous devez servir l’original, servez-le depuis une origine séparée avec une Content-Security-Policy et référencez-le par <img> plutôt que de l’intégrer. Cette page signale ; elle ne nettoie pas.

Auditer un SVG en code

Analysez le fichier en sécurité, puis parcourez-le à la recherche de ce qu’un navigateur pourrait exécuter. Chaque exemple ci-dessous désactive d’abord la résolution des entités et l’accès réseau, car un SVG portant un DOCTYPE est une charge XXE exactement comme l’est un document XML ordinaire.

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

Une liste de refus de noms de balises et d’attributs est un outil de signalement, pas une frontière de sécurité. Tout ce que vous servez réellement aux navigateurs devrait passer par un assainisseur qui analyse le document et le reconstruit à partir d’une liste d’autorisations, ou être rastérisé en PNG.

Questions fréquentes

Mon SVG est-il téléversé, et est-il affiché quelque part ?

Ni l’un ni l’autre. L’analyseur est du JavaScript exécuté dans cet onglet, donc le fichier n’est jamais transmis, et la page refuse délibérément de le rendre : pas de volet d’aperçu, pas de <img> pointé sur votre balisage, aucun chemin par lequel il atteindrait innerHTML.

Cela compte plus ici que sur les autres outils, car un SVG portant un script ne devient dangereux que lorsque quelque chose le rend : un validateur qui vous montre un aperçu exécute votre fichier non fiable dans sa propre origine. Votre saisie est conservée dans le localStorage de ce navigateur pour qu’un rechargement ne la perde pas, ce qui reste sur votre machine, est effacé par le bouton Effacer, et n’est pas fait pour les fichiers de plus de 300 000 caractères.

Un fichier SVG peut-il contenir du JavaScript ?

Oui. SVG a un élément <script>, des attributs gestionnaires d’événements comme onload et onclick sur n’importe quel élément, des valeurs href commençant par javascript:, et <foreignObject> pour du HTML embarqué. Rien de tout cela ne rend le fichier malformé ; tout fait partie du format.

Que cela s’exécute dépend de l’usage du fichier. Via une balise <img> ou un fond CSS, le script ne s’exécute pas. Intégré dans votre page, ou ouvert à sa propre URL, il s’exécute avec les privilèges de l’origine qui l’a servi : c’est ainsi qu’un téléversement d’avatar devient du cross-site scripting stocké. Assainissez avant de servir, et préférez <img> à l’intégration.

Pourquoi mon SVG passe-t-il ce contrôle tout en s’affichant vide ?

Presque toujours un espace de noms manquant. L’élément racine a besoin de xmlns="http://www.w3.org/2000/svg". Sans lui le fichier reste du XML bien formé, et il s’affiche souvent quand même collé en ligne dans du HTML parce que l’analyseur HTML devine l’espace de noms, mais chargé via <img> ou servi en image/svg+xml il ne produit rien.

Les autres causes fréquentes sont un viewBox mal orthographié viewbox, que les navigateurs ignorent puisque les noms d’attributs XML sont sensibles à la casse, un viewBox de largeur ou de hauteur nulle, et une référence à un dégradé dans un fichier qui n’est plus à côté. Aucune n’est une erreur XML, donc aucun vérificateur de syntaxe ne les signale.

Est-ce que cela valide le SVG au regard de la spécification SVG ?

Non, et c’est dit plutôt que laissé entendre. On vérifie que le fichier est du XML bien formé et que ses préfixes d’espace de noms sont déclarés. On ne vérifie pas que <circle> a un attribut r, que les données d d’un path s’analysent, ni qu’une longueur est licite.

Il existe une DTD pour SVG 1.1, et vous pouvez passer un document dessus avec le validateur de DTD d’ici, mais aucun navigateur ne valide le SVG avec cette DTD, SVG 2 n’en définit aucune, et bien des fichiers qui s’affichent parfaitement y échoueraient. La bonne formation, plus un coup d’œil à ce que le fichier exécute, voilà le contrôle qui correspond à l’usage réel de SVG.

Que signifie « le préfixe xlink n’est pas lié à un espace de noms » ?

Cela veut dire que le fichier utilise xlink:href quelque part sans déclaration xmlns:xlink="http://www.w3.org/1999/xlink" dans la portée. Un préfixe n’a aucun sens tant qu’il n’est pas lié : c’est donc une erreur fatale et non un avertissement, et tout analyseur conforme rejette le fichier.

Cela apparaît en général après avoir copié un fragment d’une feuille de sprites, en laissant la déclaration derrière soi sur la racine d’origine. La correction consiste à ajouter la déclaration à l’élément racine du fichier que vous avez. En SVG 2, l’attribut href tout court a remplacé xlink:href et n’a besoin d’aucun préfixe, et les navigateurs actuels le prennent en charge : pour un fichier que vous maîtrisez, il est souvent plus simple d’abandonner le préfixe.

Outils associés

Pour aller plus loin

Erreurs que cet outil résout