Validador DTD
Valida contra una DTD, interna o pegada. No se sube nada.
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 documento con subconjunto interno, las declaraciones que van entre los corchetes de su DOCTYPE, y pulsa Validar contra DTD. Si el DTD vive en un archivo aparte, pégalo en el panel de la derecha. En cualquier caso libxml2, compilado a WebAssembly, ejecuta la comprobación en esta pestaña e informa de elementos no declarados, violaciones del modelo de contenido y problemas de atributos con línea y columna.
Esta es la comprobación que ninguna otra herramienta gratuita en línea ofrece: los resultados para ella son la página comercial de un fabricante de escritorio, un sitio de tutoriales, un fragmento de un libro de O’Reilly y una URL de Microsoft con previous-versions en la ruta. Mientras tanto el formato se usa a diario, en edición de revistas, en las listas de propiedades de Apple, en cadenas de DocBook 4 y en una larga cola de canalizaciones EDI.
Un comportamiento que conviene conocer antes de empezar: un DTD externo referenciado por SYSTEM o PUBLIC nunca se descarga. Es deliberado, y la razón está más abajo.
Cómo se aplica un DTD pegado
Un DTD no es un documento aparte como lo es un XSD; forma parte del prólogo del documento. Así que un DTD pegado se injerta antes de analizar: la herramienta toma la primera etiqueta de apertura como nombre del DOCTYPE, descarta cualquier DOCTYPE que ya hubiera, conserva la declaración XML y escribe tus declaraciones como subconjunto interno. Reescribir en lugar de cargar un subconjunto externo significa que no existe ninguna ruta de código capaz de abrir un archivo o una URL.
Eso tiene una consecuencia, y viene de la especificación XML, no de esta herramienta. En un subconjunto interno una referencia a entidad de parámetro puede estar donde podría estar una declaración de marcado, pero no dentro de una, así que un DTD que contenga <!ELEMENT article (%block.mix;)> es legal como subconjunto externo y es un error al pegarlo aquí. Si el tuyo está ensamblado a partir de módulos .ent, como lo están JATS y DocBook, aplánalo primero.
La sintaxis, y las partes en las que la gente tropieza
Un DTD tiene cuatro tipos de declaración y una gramática corta y afilada: los elementos dan un modelo de contenido, las listas de atributos dan tipos y valores por defecto, las entidades dan sustituciones de texto, las notaciones nombran formatos externos. La forma de contenido mixto pilla a casi todo el mundo: un elemento que contiene texto e hijos debe escribirse exactamente como (#PCDATA | hijo)*, con #PCDATA primero y todo el grupo repetible. (#PCDATA, em)* es un error de sintaxis, no otro significado.
- <!ELEMENT catalog (product+)> declara hijos. Los operadores son , para secuencia y | para elección, con ? * + para opcional, cero o más y uno o más; EMPTY y ANY son los modelos especiales.
- <!ATTLIST product id ID #REQUIRED status (draft|live) "draft"> declara atributos. Los valores por defecto son #REQUIRED, #IMPLIED, #FIXED "valor", o un valor literal.
- Los tipos de atributo son CDATA, los tokenizados ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN y NMTOKENS, y las enumeraciones. Un elemento puede llevar como máximo un atributo ID.
- Los valores ID deben ser nombres XML, así que id="1" es inválido e id="n1" está bien. xs:ID en XSD hereda la misma regla.
- <!ENTITY corp "Acme Ltd."> declara una entidad general interna; SYSTEM y PUBLIC declaran externas, y NDATA marca una entidad no analizada que necesita una NOTATION.
- <!ENTITY % common SYSTEM "common.mod"> declara una entidad de parámetro, referenciada como %common;, que es el único mecanismo de modularidad del DTD.
Los DTD externos nunca se descargan, y esa es la idea
Cuando un documento dice <!DOCTYPE article SYSTEM "JATS-journalpublishing1.dtd">, un validador que lo honre tiene que ir a por ese archivo: una petición desde tu máquina, en tu red, dirigida por contenido que escribió otra persona. Aquí nada hace eso. Cada análisis usa XML_PARSE_NO_XXE y NONET, y esta compilación no registra ningún cargador de recursos, así que un identificador SYSTEM no resuelve a nada, nombre una URL o una ruta.
El DTD es la superficie de ataque clásica de XML, y por eso esto es una propiedad y no una limitación. Las declaraciones de entidad externa en un DOCTYPE son todo el XXE: un identificador file:// leído dentro de un elemento, una dirección de intranet sondeada desde dentro de tu perímetro, un canal fuera de banda construido con una entidad de parámetro. Las entidades anidadas son el vector de billion laughs, y libxml2 advierte en su propia documentación de que la validación DTD nunca debería habilitarse sobre entrada no confiable. XML_PARSE_HUGE tampoco se activa nunca, así que los techos siguen puestos: profundidad de elementos 256, anidamiento de entidades 20, y un límite de amplificación.
Dónde se siguen usando DTD de verdad
El DTD es el único lenguaje de esquema integrado en la propia especificación XML, así que todo analizador conforme valida contra uno sin ninguna dependencia extra. Por eso sobrevive donde añadir una biblioteca de esquemas no es opción. El ecosistema vivo más fuerte es la edición académica: JATS, el estándar NISO Z39.96 para XML de artículos de revista, añadió sus DOCTYPEs 1.4 en noviembre de 2024.
- JATS y BITS, en edición de revistas y libros. Se distribuyen en versiones DTD, RELAX NG y W3C Schema; el DTD es lo que comprueban la mayoría de los sistemas de ingesta.
- Listas de propiedades de Apple: todo plist de las herramientas de macOS e iOS sigue llevando el identificador PUBLIC de PLIST 1.0.
- DocBook 4.x, en cadenas que nunca se movieron. DocBook 5.0 pasó a RELAX NG, así que comprueba tu versión.
- TEI, cuyo esquema principal es RELAX NG pero que aún genera derivaciones DTD.
- Los doctypes de XHTML, hoy casi decorativos pero todavía validados en algunas canalizaciones de publicación.
- Canalizaciones antiguas de SOAP y EDI, donde un DTD se escribió una vez y el formato no ha cambiado desde entonces.
Validar contra un DTD desde código
La validación DTD pone en conflicto el valor por defecto seguro y la función que quieres: el consejo estándar de endurecimiento es prohibir las declaraciones DOCTYPE, y no puedes hacer eso y validar contra un DTD. Cada ejemplo conserva la declaración y en su lugar apaga la resolución externa.
// npm i libxml2-wasm, the same engine this page runs.
import { XmlDocument, ParseOption } from 'libxml2-wasm';
// DTDVALID turns validation on. NO_XXE keeps the external subset and any
// external entities unresolved, which is what makes this safe on input you
// did not write.
const OPTS =
ParseOption.XML_PARSE_DTDVALID |
ParseOption.XML_PARSE_NO_XXE |
ParseOption.XML_PARSE_NONET;
export function validateDtd(xmlText, dtdText) {
// A separately supplied DTD is spliced in as an internal subset rather than
// loaded, exactly as this page does it.
let source = xmlText;
if (dtdText) {
const root = xmlText.match(/<([A-Za-z_:][\w.\-:]*)[\s>/]/)?.[1] ?? 'root';
const stripped = xmlText.replace(/<!DOCTYPE[^[>]*(\[[\s\S]*?\])?\s*>/, '');
source = `<!DOCTYPE ${root} [\n${dtdText}\n]>\n${stripped.trimStart()}`;
}
let doc = null;
try {
doc = XmlDocument.fromString(source, { option: OPTS });
return { valid: true, errors: [] };
} catch (err) {
// libxml2 reports a broken document and a document that does not follow
// the DTD through the same exception. details is [{ line, col, message }].
const details = err?.details ?? [{ message: String(err?.message ?? err) }];
return { valid: false, errors: details };
} finally {
doc?.dispose();
}
}# pip install lxml
from io import StringIO
from lxml import etree
parser = etree.XMLParser(
resolve_entities=False, # do not expand entities from the DOCTYPE
no_network=True, # never fetch an external subset
huge_tree=False, # keep the depth and amplification limits
)
doc = etree.parse('article.xml', parser)
# A DTD held separately: validate without touching the document's own prolog.
with open('article.dtd', encoding='utf-8') as fh:
dtd = etree.DTD(StringIO(fh.read()))
if dtd.validate(doc):
print('valid')
else:
for e in dtd.error_log.filter_from_errors():
print(f'{e.line}:{e.column} {e.message}')
# To validate against an internal subset instead, parse with dtd_validation=True
# and read parser.error_log:
# etree.XMLParser(dtd_validation=True, no_network=True, resolve_entities=False)import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.xml.sax.InputSource;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.StringReader;
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setValidating(true); // DTD validation on
factory.setNamespaceAware(true);
factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
// disallow-doctype-decl CANNOT be used here: it would reject the DOCTYPE you
// are trying to validate against. Block the fetch, not the declaration.
factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder builder = factory.newDocumentBuilder();
// Supply the DTD text yourself instead of letting the parser go and find it.
builder.setEntityResolver((publicId, systemId) ->
new InputSource(new StringReader(dtdText)));
builder.setErrorHandler(new DefaultHandler() {
@Override public void error(SAXParseException e) {
System.out.printf("%d:%d %s%n", e.getLineNumber(), e.getColumnNumber(), e.getMessage());
}
});
builder.parse(new InputSource(new StringReader(xmlText)));using System;
using System.Xml;
var settings = new XmlReaderSettings
{
// Parse, not Prohibit: DTD validation needs the DOCTYPE read.
DtdProcessing = DtdProcessing.Parse,
ValidationType = ValidationType.DTD,
// A null resolver is what stops an external subset or entity being
// fetched. This is the single most important line here.
XmlResolver = null,
MaxCharactersFromEntities = 1024 * 1024,
MaxCharactersInDocument = 20L * 1024 * 1024,
};
settings.ValidationEventHandler += (_, e) =>
Console.WriteLine($"{e.Exception.LineNumber}:{e.Exception.LinePosition} {e.Message}");
using var reader = XmlReader.Create("article.xml", settings);
while (reader.Read()) { } // errors arrive through the handler as it reads
// With XmlResolver = null, a document whose DTD lives in a separate file will
// report that no DTD was found. Inline the declarations into the DOCTYPE first.<?php
// LIBXML_DTDVALID validates against the DOCTYPE. LIBXML_NONET blocks the fetch.
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$loaded = $doc->loadXML($source, LIBXML_DTDVALID | LIBXML_NONET);
foreach (libxml_get_errors() as $e) {
// level 1 is a warning, 2 an error, 3 fatal.
fprintf(STDERR, "%d:%d %s\n", $e->line, $e->column, trim($e->message));
}
libxml_clear_errors();
if (!$loaded) {
exit(1);
}
// DOMDocument::validate() runs the same check on an already-parsed document,
// which is useful when you want the tree even if it turns out to be invalid.# Validate against the document's own internal subset or DOCTYPE:
xmllint --noout --nonet --valid article.xml
# Validate against a DTD held separately, ignoring any DOCTYPE in the file.
# This is the form for JATS, DocBook and other modular DTDs: xmllint resolves
# the .ent modules from disk, which a browser cannot do.
xmllint --noout --nonet --dtdvalid JATS-journalpublishing1.dtd article.xml
# Exit status is 0 when the document is valid. --nonet is not the default, and
# without it xmllint will happily fetch a DTD over HTTP.Los ejemplos de Java y .NET muestran la misma tensión desde dos lados: la mitigación estándar de XXE es incompatible con la validación DTD, así que el endurecimiento que funciona es conservar la declaración y quitar el resolvedor.
Preguntas frecuentes
¿Por qué no descarga el DTD al que hace referencia mi documento?
Porque esa petición la dirigiría contenido que puede que no controles, y el DTD es donde viven los peores problemas de seguridad de XML: una entidad externa que lee un archivo local dentro de un elemento, o que sondea una dirección de intranet desde dentro de tu red. La forma segura de evitarlo es no registrar ningún resolvedor.
El coste es un paso manual: descarga el DTD tú y pégalo.
¿Sale del navegador algo de lo que pego?
No. libxml2 se ejecuta como WebAssembly en un Web Worker de esta pestaña, así que el documento y el DTD se quedan aquí. No hay punto de subida ni analítica con acceso al editor.
Eso importa para quienes todavía usan DTD: un artículo de revista bajo embargo, un plist sacado del dispositivo de un cliente, un mensaje EDI que lleva los precios de un socio. Nada de eso pinta en un envío de formulario.
¿Puedo validar contra el DTD de DocBook, JATS o XHTML?
Sí, si lo tienes como un único archivo aplanado. El DTD de nivel superior de una familia modular no funcionará: esos traen archivos .ent y .mod con entidades de parámetro e identificadores SYSTEM, y ninguna de esas referencias resuelve aquí.
Hay además una regla de la especificación: en un subconjunto interno una referencia a entidad de parámetro puede estar como declaración completa pero no dentro de una, así que (%block.mix;) es un error en lugar de una expansión. Para estas familias usa xmllint con --dtdvalid en local.
¿Debería usar un DTD o un XSD?
Usa un XSD si necesitas tipos de datos, espacios de nombres o una cardinalidad más precisa que opcional y repetible. Un DTD no puede decir que un campo es un entero entre 1 y 99, y empareja nombres prefijados literales, así que un documento que vincule el mismo espacio de nombres a otro prefijo falla.
Usa un DTD si un sistema al que alimentas exige uno, lo que cubre casi toda la edición, o si valoras que no necesita ninguna biblioteca: un DTD de 40 líneas dice tanto como 150 líneas de XSD.
¿Por qué se rechaza mi atributo id="1"?
Porque los valores ID deben ser nombres XML, y un nombre no puede empezar por dígito. Prefija el valor con una letra o un guion bajo y el error desaparece.
Dos reglas relacionadas pillan a la gente: un elemento puede llevar como máximo un atributo ID, y debe declararse #REQUIRED o #IMPLIED, nunca con un valor por defecto. xs:ID en XSD hereda la misma restricción.
¿Es seguro validar un documento que vino de otra persona?
Aquí sí, algo lo bastante inusual como para merecer decirse. Los dos riesgos con un DTD son la resolución externa y la expansión de entidades, y ambos están cerrados: no se registra ningún cargador de recursos, y XML_PARSE_HUGE nunca se activa, así que se aplican el límite de profundidad de 256, el límite de anidamiento de entidades de 20 y el tope de amplificación.
Ten más cuidado en tu propio código: Java y las versiones antiguas de PHP resuelven entidades externas por defecto.