Validateur DTD
Validez avec une DTD, interne ou collée. Rien n’est envoyé.
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 document avec un sous-ensemble interne, les déclarations placées entre les crochets de son DOCTYPE, puis appuyez sur Valider avec la DTD. Si la DTD vit dans un fichier à part, collez-la plutôt dans le volet de droite. Dans les deux cas libxml2, compilé en WebAssembly, exécute le contrôle dans cet onglet et signale les éléments non déclarés, les violations de modèle de contenu et les problèmes d’attributs avec une ligne et une colonne.
C’est le contrôle qu’aucun autre outil en ligne gratuit ne propose : ce qu’on trouve en le cherchant, c’est la page commerciale d’un éditeur de logiciel de bureau, un site de tutoriels, un extrait de livre O’Reilly et une URL Microsoft avec previous-versions dans le chemin. Pendant ce temps, le format sert tous les jours : édition de revues, listes de propriétés d’Apple, chaînes DocBook 4 et une longue traîne de pipelines EDI.
Un comportement à connaître avant de commencer : une DTD externe référencée par SYSTEM ou PUBLIC n’est jamais récupérée. C’est délibéré, et la raison est plus bas.
Comment une DTD collée est appliquée
Une DTD n’est pas un document séparé comme l’est un XSD ; elle fait partie du prologue du document. Une DTD collée est donc greffée avant l’analyse : l’outil prend la première balise ouvrante comme nom de DOCTYPE, écarte tout DOCTYPE déjà présent, conserve la déclaration XML et inscrit vos déclarations comme sous-ensemble interne. Réécrire plutôt que charger un sous-ensemble externe signifie qu’aucun chemin de code ne peut ouvrir un fichier ou une URL.
Cela a une conséquence, et elle vient de la spécification XML, pas de cet outil. Dans un sous-ensemble interne, une référence à une entité paramètre peut se trouver là où une déclaration de balisage pourrait se trouver, mais pas à l’intérieur d’une déclaration : une DTD contenant <!ELEMENT article (%block.mix;)> est donc licite comme sous-ensemble externe et devient une erreur si on la colle ici. Si la vôtre est assemblée à partir de modules .ent, comme le sont JATS et DocBook, aplatissez-la d’abord.
La syntaxe, et les points où l’on trébuche
Une DTD a quatre types de déclaration et une grammaire courte et tranchante : les éléments donnent un modèle de contenu, les listes d’attributs donnent types et valeurs par défaut, les entités donnent des substitutions de texte, les notations nomment des formats externes. La forme du contenu mixte piège presque tout le monde : un élément contenant à la fois du texte et des enfants doit s’écrire exactement (#PCDATA | enfant)*, avec #PCDATA en premier et tout le groupe répétable. (#PCDATA, em)* est une erreur de syntaxe, pas un autre sens.
- <!ELEMENT catalog (product+)> déclare des enfants. Les opérateurs sont , pour la séquence et | pour le choix, avec ? * + pour facultatif, zéro ou plus et un ou plus ; EMPTY et ANY sont les modèles spéciaux.
- <!ATTLIST product id ID #REQUIRED status (draft|live) "draft"> déclare des attributs. Les défauts sont #REQUIRED, #IMPLIED, #FIXED "valeur", ou une valeur littérale.
- Les types d’attribut sont CDATA, les types à jetons ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN et NMTOKENS, et les énumérations. Un élément ne peut porter qu’un seul attribut ID au plus.
- Les valeurs d’ID doivent être des noms XML : id="1" est donc invalide et id="n1" convient. xs:ID en XSD hérite de la même règle.
- <!ENTITY corp "Acme Ltd."> déclare une entité générale interne ; SYSTEM et PUBLIC en déclarent des externes, et NDATA marque une entité non analysée nécessitant une NOTATION.
- <!ENTITY % common SYSTEM "common.mod"> déclare une entité paramètre, référencée par %common;, qui est le seul mécanisme de modularité de la DTD.
Les DTD externes ne sont jamais récupérées, et c’est là tout le propos
Quand un document dit <!DOCTYPE article SYSTEM "JATS-journalpublishing1.dtd">, un validateur qui honore cette référence doit aller chercher ce fichier : une requête depuis votre machine, sur votre réseau, pilotée par du contenu écrit par quelqu’un d’autre. Rien ici ne fait cela. Chaque analyse utilise XML_PARSE_NO_XXE et NONET, et cette compilation n’enregistre aucun chargeur de ressources : un identifiant SYSTEM ne se résout donc en rien, qu’il nomme une URL ou un chemin.
La DTD est la surface d’attaque classique d’XML, et c’est pourquoi ceci est une propriété plutôt qu’une limitation. Les déclarations d’entité externe dans un DOCTYPE constituent tout le XXE : un identifiant file:// lu dans un élément, une adresse d’intranet sondée depuis l’intérieur de votre périmètre, un canal hors bande construit à partir d’une entité paramètre. Les entités imbriquées sont le vecteur du billion laughs, et libxml2 avertit dans sa propre documentation que la validation DTD ne devrait jamais être activée sur une entrée non fiable. XML_PARSE_HUGE n’est jamais activé non plus, donc les plafonds restent en place : profondeur d’éléments 256, imbrication d’entités 20, et une limite d’amplification.
Où les DTD servent encore vraiment
La DTD est le seul langage de schéma intégré à la spécification XML elle-même : tout analyseur conforme valide donc avec une DTD sans dépendance supplémentaire. C’est pourquoi elle survit là où ajouter une bibliothèque de schémas n’est pas envisageable. L’écosystème vivant le plus solide est l’édition savante : JATS, la norme NISO Z39.96 pour le XML d’articles de revues, a ajouté ses DOCTYPE 1.4 en novembre 2024.
- JATS et BITS, dans l’édition de revues et de livres. Livrés en versions DTD, RELAX NG et W3C Schema ; c’est la DTD que vérifient la plupart des systèmes d’ingestion.
- Les listes de propriétés d’Apple : tout plist produit par les outils macOS et iOS porte encore l’identifiant PUBLIC de PLIST 1.0.
- DocBook 4.x, dans les chaînes qui n’ont jamais bougé. DocBook 5.0 est passé à RELAX NG, vérifiez donc votre version.
- TEI, dont le schéma principal est RELAX NG mais qui génère encore des dérivations DTD.
- Les doctypes XHTML, surtout décoratifs aujourd’hui mais encore validés dans certains pipelines d’édition.
- Les vieux pipelines SOAP et EDI, où une DTD a été écrite une fois et où le format n’a pas changé depuis.
Valider avec une DTD en code
La validation DTD met en conflit le réglage sûr par défaut et la fonctionnalité recherchée : le conseil de durcissement standard est d’interdire les déclarations DOCTYPE, et vous ne pouvez pas faire cela tout en validant avec une DTD. Chaque exemple conserve la déclaration et coupe plutôt la résolution externe.
// 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.Les exemples Java et .NET montrent la même tension par deux bouts : l’atténuation XXE standard est incompatible avec la validation DTD, donc le durcissement qui marche consiste à garder la déclaration et à retirer le résolveur.
Questions fréquentes
Pourquoi ne récupère-t-il pas la DTD que mon document référence ?
Parce que cette requête serait pilotée par du contenu que vous ne contrôlez peut-être pas, et que la DTD est l’endroit où vivent les pires problèmes de sécurité d’XML : une entité externe qui lit un fichier local dans un élément, ou qui sonde une adresse d’intranet depuis l’intérieur de votre réseau. Le moyen sûr de l’empêcher est de n’enregistrer aucun résolveur.
Le coût est une étape manuelle : récupérez la DTD vous-même et collez-la.
Ce que je colle quitte-t-il le navigateur ?
Non. libxml2 tourne en WebAssembly dans un Web Worker de cet onglet : le document et la DTD restent tous deux ici. Il n’y a pas de point de téléversement ni d’analytics ayant accès à l’éditeur.
Cela compte pour ceux qui utilisent encore des DTD : un article de revue sous embargo, un plist extrait de l’appareil d’un client, un message EDI portant les tarifs d’un partenaire. Rien de tout cela n’a sa place dans un envoi de formulaire.
Puis-je valider avec la DTD de DocBook, de JATS ou de XHTML ?
Oui, si vous l’avez sous forme d’un seul fichier aplati. La DTD de plus haut niveau d’une famille modulaire ne marchera pas : celles-ci tirent des fichiers .ent et .mod via des entités paramètres et des identifiants SYSTEM, et aucune de ces références ne se résout ici.
Il y a aussi une règle de la spécification : dans un sous-ensemble interne, une référence à une entité paramètre peut tenir lieu de déclaration entière mais pas figurer à l’intérieur d’une déclaration, donc (%block.mix;) est une erreur et non une expansion. Pour ces familles, utilisez xmllint avec --dtdvalid en local.
Faut-il utiliser une DTD ou un XSD ?
Utilisez un XSD s’il vous faut des types de données, des espaces de noms, ou une cardinalité plus précise que facultatif et répétable. Une DTD ne peut pas dire qu’un champ est un entier entre 1 et 99, et elle compare des noms préfixés littéraux : un document qui lie le même espace de noms à un autre préfixe échoue.
Utilisez une DTD si un système que vous alimentez en exige une, ce qui couvre l’essentiel de l’édition, ou si vous appréciez qu’elle ne demande aucune bibliothèque : une DTD de 40 lignes en dit autant que 150 lignes de XSD.
Pourquoi mon attribut id="1" est-il rejeté ?
Parce que les valeurs d’ID doivent être des noms XML, et qu’un nom ne peut pas commencer par un chiffre. Préfixez la valeur par une lettre ou un tiret bas et l’erreur disparaît.
Deux règles voisines piègent les gens : un élément ne peut porter qu’un seul attribut ID au plus, et celui-ci doit être déclaré #REQUIRED ou #IMPLIED, jamais avec une valeur par défaut. xs:ID en XSD hérite de la même contrainte.
Est-il sûr de valider un document venu de quelqu’un d’autre ?
Ici, oui, ce qui est assez rare pour mériter d’être dit. Les deux risques d’une DTD sont la résolution externe et l’expansion d’entités, et les deux sont fermés : aucun chargeur de ressources n’est enregistré, et XML_PARSE_HUGE n’est jamais activé, donc la limite de profondeur de 256, celle d’imbrication d’entités de 20 et le plafond d’amplification s’appliquent tous.
Soyez plus prudent dans votre propre code : Java et les anciennes versions de PHP résolvent les entités externes par défaut.