Validador SVG

Valida o SVG como XML e sinaliza o executável. Nunca renderiza.

Entrada
AguardandoCole um documento para verificá-lo. A validação roda enquanto você digita.

Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.

Cole um SVG acima, ou solte o arquivo sobre o editor, e ele é verificado como XML: todos os erros de sintaxe de uma vez, cada um com a sua linha, a sua coluna e a correção. SVG é um vocabulário XML, então um SVG que uma ferramenta de build, um pipeline de ícones ou um navegador rejeita costuma estar desrespeitando as regras do XML, e não as do SVG.

Recorra a ele quando uma exportação do Figma, do Illustrator ou do Inkscape quebra um empacotador, quando um ícone aparece em branco e você precisa saber se o arquivo sequer é analisado, ou quando alguém subiu um SVG e você quer lê-lo antes que ele chegue perto de um navegador.

A diferença aqui está no que a página se recusa a fazer. A maioria dos validadores de SVG desenha o seu arquivo na tela ao lado do código, entregando-o ao mesmo motor de renderização que executaria qualquer script dentro dele. Esta página nunca o renderiza. A sua marcação continua sendo texto num editor: todo valor que a página exibe é escrito com textContent ou createElement, então nenhuma parte do arquivo chega ao innerHTML, e nada é enviado.

SVG é XML que os navegadores executam

Um SVG pode carregar um elemento <script>, um atributo onload ou onmouseover em qualquer forma, um href começando com javascript:, e um <foreignObject> contendo HTML arbitrário. Tudo isso é SVG legítimo e tudo isso é XML bem formado, e é por isso que uma verificação de sintaxe sozinha nunca pode lhe dizer que um arquivo é seguro.

Quando esse conteúdo roda depende de como o arquivo é usado. Carregado por uma tag <img> ou por um fundo CSS, um SVG não executa script nenhum e não alcança a página ao redor. Embutido no seu DOM, ou aberto na própria URL, ele executa na origem que o serviu. Esse segundo caso transforma um recurso de upload em cross-site scripting armazenado.

Então esta página verifica o XML e se recusa a ser a coisa que roda o arquivo. Um elemento <script> não é relatado como erro de sintaxe, porque não é um. Você recebe a marcação como texto realçado com todo problema de boa formação marcado, e o código de auditoria abaixo para pôr uma verificação de conteúdo executável no passo de build, que é o lugar dela.

As falhas de SVG que são falhas de XML

Elas respondem por quase todo SVG que um analisador rejeita, cada uma relatada com uma posição exata em vez de um único «SVG inválido».

  • xlink:href usado sem xmlns:xlink="http://www.w3.org/1999/xlink" no escopo. Um prefixo não vinculado é erro fatal pelo Namespaces in XML, e aparece o tempo todo em fragmentos tirados de uma folha de sprites.
  • Um <path>, <image>, <use> ou <stop> deixado sem fechar. HTML tem elementos vazios; XML não, então <path d="..."> sem a barra de fechamento é uma tag aberta, e o erro só aflora muito depois, na tag que não conseguiu casar.
  • Um e comercial cru numa query string de xlink:href, que dentro de um valor de atributo precisa ser escrito como &amp;, ou um < literal num atributo style, que o XML proíbe ali mesmo permitindo o >.
  • O mesmo atributo posto duas vezes num elemento, o que alguns pipelines de exportação produzem quando fundem transformações de forma desastrada.
  • O velho DOCTYPE do Illustrator apontando para a DTD do SVG 1.1. Ele é relatado e nunca buscado, o que também explica por que nada aqui é validado contra aquela DTD.
  • Um subconjunto interno de entidades que se expande em centenas de megabytes. O SVG é um veículo popular para esse ataque porque formulários de upload o aceitam; o custo é calculado a partir das declarações e o arquivo é rejeitado em uns dois milissegundos.

O que uma verificação de boa formação não consegue lhe dizer

Ser explícito quanto a isso é mais útil que um tique verde. Todos os arquivos abaixo são XML bem formado, passam aqui, e continuam quebrados como SVG.

  • Um elemento raiz sem xmlns="http://www.w3.org/2000/svg". Embutido no HTML normalmente ainda renderiza, porque o analisador de HTML supõe o espaço de nomes do SVG; via <img> ou servido como image/svg+xml não renderiza nada. Esta é a causa mais comum de «meu SVG está em branco».
  • viewbox em vez de viewBox. Nomes de atributo XML diferenciam maiúsculas e o SVG define apenas a grafia em camel case, então os navegadores ignoram o erro de grafia e o ícone escala errado em vez de falhar.
  • width e height definidos sem viewBox, o que produz uma imagem que não escala, e um viewBox com largura ou altura zero, que não produz nada.
  • Um xlink:href apontando para outro arquivo para um gradiente, um filtro ou uma referência <use>. Ele resolve enquanto o arquivo está no seu disco e quebra no instante em que o SVG é embutido ou servido de outra origem, assim como um elemento <text> composto numa fonte que só existe na máquina do designer.

Antes que um SVG enviado chegue a um navegador

Se o arquivo veio de um usuário, trate lê-lo aqui como o primeiro passo, não o último. Procurar pela palavra script não é defesa: cargas se escondem em data URIs em base64, em <set attributeName="onload">, em elementos animate, e em truques de espaço de nomes pelos quais um filtro ingênuo passa batido.

As opções confiáveis são um sanitizador que analisa em vez de fazer grep — DOMPurify com seu perfil SVG no Node ou no navegador, e enshrined/svg-sanitize no PHP — ou rasterizar para PNG no servidor. Se precisar servir o original, sirva-o de uma origem separada com Content-Security-Policy definida e referencie-o por <img> em vez de embutir. Esta página relata; ela não limpa.

Auditar um SVG em código

Analise o arquivo com segurança e depois percorra-o procurando o que um navegador poderia executar. Cada exemplo abaixo desliga primeiro a resolução de entidades e o acesso à rede, porque um SVG que carrega um DOCTYPE é uma carga de XXE exatamente como é um documento XML comum.

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

Uma lista de negação de nomes de tag e de atributo é uma ferramenta de relato, não uma fronteira de segurança. Tudo o que você realmente servir a navegadores deveria passar por um sanitizador que analisa o documento e o reconstrói a partir de uma lista de permissão, ou ser rasterizado para PNG.

Perguntas frequentes

Meu SVG é enviado, e é exibido em algum lugar?

Nem uma coisa nem outra. O analisador é JavaScript rodando nesta aba, então o arquivo nunca é transmitido, e a página deliberadamente não o renderiza: sem painel de pré-visualização, sem um <img> apontado para a sua marcação, sem caminho pelo qual ele chegue ao innerHTML.

Isso importa mais aqui do que nas outras ferramentas, porque um SVG que carrega um script só se torna perigoso quando algo o renderiza: um validador que lhe mostra uma pré-visualização está rodando o seu arquivo não confiável na própria origem. Sua entrada fica no localStorage deste navegador para que um recarregamento não a perca, o que permanece na sua máquina, é apagado pelo botão Limpar, e é pulado para arquivos com mais de 300.000 caracteres.

Um arquivo SVG pode conter JavaScript?

Pode. O SVG tem um elemento <script>, atributos manipuladores de evento como onload e onclick em qualquer elemento, valores href começando com javascript:, e <foreignObject> para HTML embutido. Nada disso torna o arquivo malformado; tudo faz parte do formato.

Se roda ou não depende de como o arquivo é usado. Por uma tag <img> ou por um fundo CSS, o script não executa. Embutido na sua página, ou aberto na própria URL, ele executa com os privilégios da origem que o serviu, e é assim que um upload de avatar vira cross-site scripting armazenado. Sanitize antes de servir, e prefira <img> a embutir.

Por que meu SVG passa nesta verificação e mesmo assim aparece em branco?

Quase sempre é um espaço de nomes faltando. O elemento raiz precisa de xmlns="http://www.w3.org/2000/svg". Sem ele o arquivo continua sendo XML bem formado, e muitas vezes ainda renderiza quando colado direto no HTML, porque o analisador de HTML adivinha o espaço de nomes, mas carregado por <img> ou servido como image/svg+xml não produz nada.

As outras causas frequentes são um viewBox grafado como viewbox, que os navegadores ignoram porque nomes de atributo XML diferenciam maiúsculas, um viewBox com largura ou altura zero, e uma referência a um gradiente num arquivo que já não está ao lado. Nenhuma delas é erro de XML, então nenhum verificador de sintaxe as relata.

Isto valida SVG contra a especificação do SVG?

Não, e isso é dito em vez de insinuado. Verifica-se que o arquivo é XML bem formado e que os seus prefixos de espaço de nomes estão declarados. Não se verifica que <circle> tenha um atributo r, que os dados d de um path sejam analisáveis, nem que um comprimento seja legítimo.

Existe uma DTD para o SVG 1.1, e você pode passar um documento por ela no validador de DTD daqui, mas nenhum navegador valida SVG contra aquela DTD, o SVG 2 não define nenhuma, e muitos arquivos que renderizam perfeitamente não passariam. Boa formação, mais um olhar no que o arquivo executa, é a verificação que corresponde ao uso real do SVG.

O que significa «o prefixo xlink não está vinculado a um espaço de nomes»?

Significa que o arquivo usa xlink:href em algum lugar sem uma declaração xmlns:xlink="http://www.w3.org/1999/xlink" no escopo. Um prefixo não tem significado enquanto não é vinculado, então isso é erro fatal e não um aviso, e todo analisador conforme rejeita o arquivo.

Costuma aparecer depois que um fragmento foi copiado de uma folha de sprites, deixando a declaração para trás na raiz original. A correção é acrescentar a declaração ao elemento raiz do arquivo que você tem. No SVG 2 o atributo href simples substituiu o xlink:href e não precisa de prefixo, e os navegadores atuais o suportam, então para um arquivo que você controla costuma ser mais simples largar o prefixo.

Ferramentas relacionadas

Leitura complementar

Erros que isto resolve