Validatore SVG

Convalida l’SVG come XML e segnala l’eseguibile. Non lo disegna mai.

Input
In attesaIncolla un documento per controllarlo. La convalida gira mentre scrivi.

Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.

Incolla un SVG qui sopra, oppure trascina il file sull’editor: viene controllato come XML, tutti gli errori di sintassi in una volta, ciascuno con la sua riga, la sua colonna e la correzione. SVG è un vocabolario XML, quindi un SVG che uno strumento di build, una pipeline di icone o un browser rifiuta di solito viola le regole di XML più che quelle di SVG.

Usalo quando un export da Figma, Illustrator o Inkscape rompe un bundler, quando un’icona appare vuota e devi sapere se il file si analizza del tutto, oppure quando un utente ha caricato un SVG e vuoi leggerlo prima che si avvicini a un browser.

La differenza qui sta in ciò che questa pagina si rifiuta di fare. Quasi tutti i validatori SVG disegnano il tuo file sullo schermo accanto al sorgente, consegnandolo allo stesso motore di rendering che eseguirebbe qualunque script contenuto. Questa pagina non lo disegna mai. Il tuo markup resta testo dentro un editor: ogni valore che la pagina mostra è scritto con textContent o createElement, quindi nessuna parte del file arriva a innerHTML, e nulla viene caricato.

SVG è XML che i browser eseguono

Un SVG può portare un elemento <script>, un attributo onload o onmouseover su qualsiasi forma, un href che comincia con javascript:, e un <foreignObject> che contiene HTML arbitrario. Tutto questo è SVG lecito e tutto questo è XML ben formato: ecco perché un controllo di sintassi da solo non potrà mai dirti che un file è sicuro.

Quando quel contenuto viene eseguito dipende da come si usa il file. Caricato tramite un tag <img> o uno sfondo CSS, un SVG non esegue alcuno script e non può raggiungere la pagina circostante. Incorporato nel tuo DOM, o aperto al proprio URL, viene eseguito nell’origine che lo ha servito. Questo secondo caso trasforma una funzione di caricamento in cross-site scripting persistente.

Perciò questa pagina controlla l’XML e rifiuta di essere la cosa che esegue il file. Un elemento <script> non viene segnalato come errore di sintassi, perché non lo è. Ottieni il markup come testo evidenziato con ogni problema di buona formazione marcato, e il codice di audit qui sotto per mettere un controllo del contenuto eseguibile dove gli compete: nel passo di build.

I fallimenti SVG che sono fallimenti XML

Spiegano quasi ogni SVG che un parser rifiuta, e ciascuno viene segnalato con una posizione esatta invece che con un unico «SVG non valido».

  • xlink:href usato senza xmlns:xlink="http://www.w3.org/1999/xlink" nello scope. Un prefisso non legato è un errore fatale secondo Namespaces in XML, e salta fuori di continuo nei frammenti estratti da uno sprite sheet.
  • Un <path>, <image>, <use> o <stop> lasciato non chiuso. HTML ha elementi vuoti; XML no, quindi <path d="..."> senza barra di chiusura è un tag non chiuso, e l’errore emerge molto più avanti, sul tag che non è riuscito a corrispondere.
  • Una e commerciale nuda in una query string di xlink:href, che dentro un valore di attributo va scritta &amp;, oppure un < letterale in un attributo style, che XML lì vieta anche se il > è ammesso.
  • Lo stesso attributo indicato due volte su un elemento, cosa che alcune pipeline di esportazione producono quando uniscono male le trasformazioni.
  • Il vecchio DOCTYPE di Illustrator che punta alla DTD di SVG 1.1. Viene segnalato e mai scaricato, il che spiega anche perché qui nulla venga convalidato con quella DTD.
  • Un sottoinsieme di entità interne che si espande in centinaia di megabyte. SVG è un vettore popolare per quell’attacco perché i moduli di caricamento lo accettano; il costo si calcola dalle dichiarazioni e il file viene rifiutato in circa due millisecondi.

Ciò che un controllo di buona formazione non può dirti

Dirlo esplicitamente è più utile di una spunta verde. Ognuno dei file qui sotto è XML ben formato, passa qui, e resta rotto come SVG.

  • Un elemento radice senza xmlns="http://www.w3.org/2000/svg". Incorporato nell’HTML di solito si disegna lo stesso, perché il parser HTML presume il namespace SVG; tramite <img> o servito come image/svg+xml non disegna nulla. È la causa più comune del «il mio SVG è vuoto».
  • viewbox invece di viewBox. I nomi di attributo XML distinguono maiuscole e minuscole e SVG definisce solo la grafia in camel case, quindi i browser ignorano l’errore e l’icona si ridimensiona male invece di fallire.
  • width e height impostati senza viewBox, che produce un’immagine che non si ridimensiona, e un viewBox con larghezza o altezza zero, che non produce nulla.
  • Un xlink:href che punta a un altro file per un gradiente, un filtro o un riferimento <use>. Si risolve finché il file sta sul tuo disco e si rompe nel momento in cui l’SVG viene incorporato o servito da un’altra origine, come accade a un elemento <text> composto con un font che esiste solo sulla macchina del designer.

Prima che un SVG caricato raggiunga un browser

Se il file arriva da un utente, considera leggerlo qui un primo passo e non l’ultimo. Cercare la parola script non è una difesa: i payload si nascondono in data URI in base64, in <set attributeName="onload">, negli elementi animate e in trucchi di namespace che un filtro ingenuo scavalca.

Le opzioni affidabili sono un sanificatore che analizza invece di fare grep — DOMPurify con il suo profilo SVG in Node o nel browser, ed enshrined/svg-sanitize in PHP — oppure la rasterizzazione in PNG lato server. Se devi servire l’originale, servilo da un’origine separata con una Content-Security-Policy impostata e referenzialo con <img> invece di incorporarlo. Questa pagina segnala; non ripulisce.

Controllare un SVG da codice

Analizza il file in modo sicuro, poi percorrilo cercando ciò che un browser potrebbe eseguire. Ogni esempio qui sotto disattiva prima la risoluzione delle entità e l’accesso alla rete, perché un SVG che porta un DOCTYPE è un payload XXE esattamente come lo è un normale documento XML.

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

Un elenco di nomi di tag e attributi vietati è uno strumento di segnalazione, non un confine di sicurezza. Tutto ciò che servi davvero ai browser dovrebbe passare per un sanificatore che analizza il documento e lo ricostruisce da un elenco di elementi ammessi, oppure essere rasterizzato in PNG.

Domande frequenti

Il mio SVG viene caricato da qualche parte, e viene mostrato?

Né l’uno né l’altro. Il parser è JavaScript che gira in questa scheda, quindi il file non viene mai trasmesso, e la pagina di proposito non lo disegna: nessun riquadro di anteprima, nessun <img> puntato sul tuo markup, nessun percorso attraverso cui arrivi a innerHTML.

Qui conta più che negli altri strumenti, perché un SVG che porta uno script diventa pericoloso solo quando qualcosa lo disegna: un validatore che ti mostra un’anteprima sta eseguendo il tuo file non fidato nella propria origine. Il tuo input resta nel localStorage di questo browser perché un aggiornamento non lo perda, cosa che rimane sulla tua macchina, viene rimossa dal pulsante Pulisci e viene saltata per i file oltre i 300.000 caratteri.

Un file SVG può contenere JavaScript?

Sì. SVG ha un elemento <script>, attributi gestori di eventi come onload e onclick su qualsiasi elemento, valori href che iniziano con javascript:, e <foreignObject> per HTML incorporato. Niente di tutto ciò rende il file malformato; fa tutto parte del formato.

Se venga eseguito dipende da come si usa il file. Tramite un tag <img> o uno sfondo CSS lo script non viene eseguito. Incorporato nella tua pagina, o aperto al proprio URL, viene eseguito con i privilegi dell’origine che lo ha servito: è così che il caricamento di un avatar diventa cross-site scripting persistente. Sanifica prima di servire, e preferisci <img> all’incorporamento.

Perché il mio SVG passa questo controllo e si vede comunque vuoto?

Quasi sempre manca il namespace. L’elemento radice ha bisogno di xmlns="http://www.w3.org/2000/svg". Senza, il file resta XML ben formato, e spesso si disegna lo stesso se incollato in linea nell’HTML perché il parser HTML indovina il namespace, ma caricato tramite <img> o servito come image/svg+xml non produce nulla.

Le altre cause frequenti sono un viewBox scritto viewbox, che i browser ignorano perché i nomi di attributo XML distinguono maiuscole e minuscole, un viewBox con larghezza o altezza zero, e un riferimento a un gradiente in un file che non gli sta più accanto. Nessuna è un errore XML, quindi nessun controllore di sintassi le segnala.

Questo convalida l’SVG rispetto alla specifica SVG?

No, e lo dice invece di lasciarlo intendere. Controlla che il file sia XML ben formato e che i suoi prefissi di namespace siano dichiarati. Non controlla che <circle> abbia un attributo r, che i dati d di un path si analizzino, né che una lunghezza sia lecita.

Esiste una DTD per SVG 1.1, e puoi passarci un documento con il validatore DTD di questo sito, ma nessun browser convalida l’SVG con quella DTD, SVG 2 non ne definisce alcuna, e molti file che si disegnano perfettamente non la passerebbero. La buona formazione, più uno sguardo a ciò che il file esegue, è il controllo che corrisponde all’uso reale di SVG.

Che cosa significa «il prefisso xlink non è legato a un namespace»?

Significa che il file usa xlink:href da qualche parte senza una dichiarazione xmlns:xlink="http://www.w3.org/1999/xlink" nello scope. Un prefisso non ha significato finché non viene legato, quindi questo è un errore fatale e non un avviso, e ogni parser conforme rifiuta il file.

Di solito compare dopo aver copiato un frammento da uno sprite sheet, lasciando la dichiarazione sulla radice originale. La correzione è aggiungere la dichiarazione all’elemento radice del file che hai. In SVG 2 il semplice attributo href ha sostituito xlink:href e non richiede prefisso, e i browser attuali lo supportano: per un file che controlli tu, spesso è più semplice abbandonare il prefisso.

Strumenti correlati

Approfondimenti

Errori che risolve