Validador SVG
Valida el SVG como XML y marca lo ejecutable. Nunca lo renderiza.
Todo se ejecuta en esta pestaña. Nada de lo que pegues se sube, se registra ni se envía a ningún sitio. Abre el panel de red y compruébalo.
Pega un SVG arriba, o suelta el archivo sobre el editor, y se comprueba como XML: todos los errores de sintaxis a la vez, cada uno con su línea, su columna y la corrección. SVG es un vocabulario XML, así que un SVG que rechaza una herramienta de compilación, una canalización de iconos o un navegador suele estar incumpliendo las reglas de XML antes que las de SVG.
Recurre a ella cuando una exportación de Figma, Illustrator o Inkscape rompe un empaquetador, cuando un icono se ve en blanco y necesitas saber si el archivo siquiera se analiza, o cuando un usuario ha subido un SVG y quieres leerlo antes de que se acerque a un navegador.
La diferencia aquí está en lo que la página se niega a hacer. La mayoría de validadores de SVG dibujan tu archivo en pantalla junto al código, entregándoselo al mismo motor de renderizado que ejecutaría cualquier script que lleve dentro. Esta página nunca lo renderiza. Tu marcado se queda como texto en un editor: cada valor que la página muestra se escribe con textContent o createElement, así que ninguna parte del archivo llega a innerHTML, y nada se sube.
SVG es XML que los navegadores ejecutan
Un SVG puede llevar un elemento <script>, un atributo onload u onmouseover en cualquier forma, un href que empiece por javascript:, y un <foreignObject> con HTML arbitrario dentro. Todo eso es SVG legal y todo eso es XML bien formado, y por eso una comprobación de sintaxis por sí sola nunca puede decirte que un archivo es seguro.
Cuándo se ejecuta ese contenido depende de cómo se use el archivo. Cargado a través de una etiqueta <img> o de un fondo CSS, un SVG no ejecuta ningún script y no puede alcanzar la página que lo rodea. Insertado en tu DOM, o abierto en su propia URL, se ejecuta en el origen que lo sirvió. Ese segundo caso convierte una función de subida en cross-site scripting almacenado.
Así que esta página comprueba el XML y se niega a ser la cosa que ejecuta el archivo. Un elemento <script> no se informa como error de sintaxis, porque no lo es. Recibes el marcado como texto resaltado con cada problema de buena formación marcado, y el código de auditoría de abajo para poner una comprobación de contenido ejecutable en el paso de compilación, que es donde corresponde.
Los fallos de SVG que son fallos de XML
Estos explican casi todos los SVG que un analizador rechaza, y cada uno se informa con una posición exacta en lugar de un único «SVG inválido».
- xlink:href usado sin xmlns:xlink="http://www.w3.org/1999/xlink" en ámbito. Un prefijo sin vincular es un error grave según Namespaces in XML, y aparece constantemente en fragmentos sacados de una hoja de sprites.
- Un <path>, <image>, <use> o <stop> sin cerrar. HTML tiene elementos vacíos; XML no, así que <path d="..."> sin la barra de cierre es una etiqueta sin cerrar, y el error asoma mucho después, en la etiqueta que no logró emparejar.
- Un ampersand crudo en una cadena de consulta de xlink:href, que dentro del valor de un atributo debe escribirse como &, o un < literal en un atributo style, que XML prohíbe ahí aunque > sí se permita.
- El mismo atributo puesto dos veces en un elemento, algo que producen algunas canalizaciones de exportación cuando fusionan mal las transformaciones.
- El viejo DOCTYPE de Illustrator que apunta al DTD de SVG 1.1. Se informa y nunca se descarga, que es también la razón por la que aquí nada se valida contra ese DTD.
- Un subconjunto interno de entidades que se expande en cientos de megabytes. SVG es un portador popular para ese ataque porque los formularios de subida lo aceptan; el coste se calcula a partir de las declaraciones y el archivo se rechaza en unos dos milisegundos.
Lo que una comprobación de buena formación no puede decirte
Ser explícito sobre esto es más útil que una marca verde. Todos los archivos de abajo son XML bien formado, pasan aquí, y siguen estando rotos como SVG.
- Un elemento raíz sin xmlns="http://www.w3.org/2000/svg". Inline en HTML normalmente se renderiza igual, porque el analizador de HTML supone el espacio de nombres de SVG; a través de <img> o servido como image/svg+xml no renderiza nada. Esta es la causa más común de «mi SVG sale en blanco».
- viewbox en lugar de viewBox. Los nombres de atributo XML distinguen mayúsculas y SVG define solo la escritura en camel case, así que los navegadores ignoran la falta y el icono escala mal en vez de fallar.
- width y height puestos sin viewBox, lo que produce una imagen que no escala, y un viewBox con anchura o altura cero, que no produce nada.
- Un xlink:href que apunta a otro archivo para un degradado, un filtro o una referencia <use>. Resuelve mientras el archivo está en tu disco y se rompe en cuanto el SVG se inserta o se sirve desde otro origen, igual que un elemento <text> compuesto con una tipografía que solo existe en la máquina del diseñador.
Antes de que un SVG subido llegue a un navegador
Si el archivo vino de un usuario, trata leerlo aquí como un primer paso y no como el último. Buscar la palabra script no es defensa: las cargas útiles se esconden en URIs de datos en base64, en <set attributeName="onload">, en elementos animate y en trucos de espacios de nombres por los que un filtro ingenuo pasa de largo.
Las opciones fiables son un saneador que analiza en vez de buscar cadenas —DOMPurify con su perfil SVG en Node o el navegador, y enshrined/svg-sanitize en PHP— o rasterizar a PNG en el servidor. Si tienes que servir el original, sírvelo desde un origen aparte con Content-Security-Policy puesta y refiérelo con <img> en vez de insertarlo. Esta página informa; no limpia.
Auditar un SVG desde código
Analiza el archivo de forma segura y luego recórrelo buscando lo que un navegador podría ejecutar. Cada ejemplo de abajo desactiva primero la resolución de entidades y el acceso a red, porque un SVG que lleva un DOCTYPE es una carga XXE exactamente igual que lo es un documento XML corriente.
// 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.Una lista de denegación de nombres de etiqueta y atributo es una herramienta de informe, no una frontera de seguridad. Todo lo que realmente sirvas a navegadores debería pasar por un saneador que analice el documento y lo reconstruya a partir de una lista de permitidos, o ser rasterizado a PNG.
Preguntas frecuentes
¿Se sube mi SVG, y se muestra en algún sitio?
Ninguna de las dos cosas. El analizador es JavaScript ejecutándose en esta pestaña, así que el archivo nunca se transmite, y la página deliberadamente no lo renderiza: sin panel de vista previa, sin un <img> apuntando a tu marcado, sin ninguna ruta por la que llegue a innerHTML.
Eso importa más aquí que en las otras herramientas, porque un SVG que lleva un script solo se vuelve peligroso cuando algo lo renderiza: un validador que te enseña una vista previa está ejecutando tu archivo no confiable en su propio origen. Tu entrada se guarda en el localStorage de este navegador para que una recarga no la pierda, lo cual se queda en tu máquina, lo borra el botón Limpiar, y se omite para archivos de más de 300.000 caracteres.
¿Puede un archivo SVG contener JavaScript?
Sí. SVG tiene un elemento <script>, atributos manejadores de eventos como onload y onclick en cualquier elemento, valores href que empiezan por javascript:, y <foreignObject> para HTML incrustado. Nada de eso hace que el archivo esté mal formado; todo forma parte del formato.
Que se ejecute depende de cómo se use el archivo. A través de una etiqueta <img> o de un fondo CSS, el script no se ejecuta. Insertado en tu página, o abierto en su propia URL, se ejecuta con los privilegios del origen que lo sirvió, y así es como una subida de avatar se convierte en cross-site scripting almacenado. Sanea antes de servir, y prefiere <img> a insertarlo.
¿Por qué mi SVG pasa esta comprobación y aun así se ve en blanco?
Casi siempre es un espacio de nombres que falta. El elemento raíz necesita xmlns="http://www.w3.org/2000/svg". Sin él el archivo sigue siendo XML bien formado, y a menudo se renderiza igual al pegarlo inline en HTML porque el analizador de HTML adivina el espacio de nombres, pero cargado con <img> o servido como image/svg+xml no produce nada.
Las otras causas frecuentes son un viewBox mal escrito como viewbox, que los navegadores ignoran porque los nombres de atributo XML distinguen mayúsculas, un viewBox con anchura o altura cero, y una referencia a un degradado en un archivo que ya no está al lado. Ninguna es un error de XML, así que ningún comprobador de sintaxis las informa.
¿Esto valida SVG contra la especificación de SVG?
No, y lo dice en lugar de dar a entender otra cosa. Comprueba que el archivo sea XML bien formado y que sus prefijos de espacio de nombres estén declarados. No comprueba que <circle> tenga un atributo r, que los datos d de un path se analicen, ni que una longitud sea legal.
Existe un DTD para SVG 1.1, y puedes pasar un documento por él en el validador de DTD de aquí, pero ningún navegador valida SVG contra ese DTD, SVG 2 no define ninguno, y muchos archivos que se renderizan perfectamente no lo pasarían. La buena formación, más una mirada a lo que el archivo ejecuta, es la comprobación que encaja con cómo se usa SVG de verdad.
¿Qué significa «el prefijo xlink no está vinculado a un espacio de nombres»?
Significa que el archivo usa xlink:href en algún sitio sin una declaración xmlns:xlink="http://www.w3.org/1999/xlink" en ámbito. Un prefijo no tiene significado hasta que se vincula, así que esto es un error fatal y no un aviso, y todo analizador conforme rechaza el archivo.
Suele aparecer después de copiar un fragmento de una hoja de sprites, dejando la declaración atrás en la raíz original. El arreglo es añadir la declaración al elemento raíz del archivo que tienes. En SVG 2 el atributo href a secas sustituyó a xlink:href y no necesita prefijo, y los navegadores actuales lo admiten, así que para un archivo que controlas suele ser más simple quitar el prefijo.