DTD-Validator
Gegen eine DTD prüfen, intern oder eingefügt. Kein Upload.
Alles läuft in diesem Tab. Nichts, was Sie einfügen, wird hochgeladen, protokolliert oder irgendwohin gesendet. Öffnen Sie Ihr Netzwerkpanel und prüfen Sie es.
Fügen Sie ein Dokument mit internem Subset ein – den Deklarationen zwischen den eckigen Klammern seines DOCTYPE – und drücken Sie „Gegen DTD prüfen“. Liegt die DTD in einer eigenen Datei, fügen Sie sie stattdessen im rechten Bereich ein. So oder so führt libxml2, nach WebAssembly kompiliert, die Prüfung in diesem Tab aus und meldet nicht deklarierte Elemente, Verstöße gegen das Inhaltsmodell und Attributprobleme mit Zeile und Spalte.
Das ist die Prüfung, die kein anderes kostenloses Online-Werkzeug anbietet: Was die Suche danach zutage fördert, ist die Werbeseite eines Desktop-Anbieters, eine Tutorialseite, ein Buchauszug von O’Reilly und eine Microsoft-URL mit previous-versions im Pfad. Währenddessen ist das Format täglich im Einsatz: im Zeitschriftenverlag, in Apples Property Lists, in DocBook-4-Toolchains und in einem langen Ausläufer von EDI-Pipelines.
Ein Verhalten, das Sie vorher kennen sollten: Eine über SYSTEM oder PUBLIC referenzierte externe DTD wird niemals geladen. Das ist Absicht, und der Grund steht weiter unten.
Wie eine eingefügte DTD angewandt wird
Eine DTD ist kein eigenständiges Dokument wie ein XSD; sie gehört zum Prolog des Dokuments. Eine eingefügte DTD wird deshalb vor dem Parsen eingesetzt: Das Werkzeug nimmt das erste Start-Tag als DOCTYPE-Namen, verwirft ein schon vorhandenes DOCTYPE, behält die XML-Deklaration und schreibt Ihre Deklarationen als internes Subset hinein. Umschreiben statt ein externes Subset zu laden heißt, dass es gar keinen Codepfad gibt, der eine Datei oder eine URL öffnen könnte.
Das hat eine Folge, und sie stammt aus der XML-Spezifikation, nicht aus diesem Werkzeug. In einem internen Subset darf eine Parameter-Entity-Referenz dort stehen, wo eine Markup-Deklaration stehen könnte, aber nicht innerhalb einer solchen. Eine DTD mit <!ELEMENT article (%block.mix;)> ist also als externes Subset zulässig und hier eingefügt ein Fehler. Ist Ihre DTD aus .ent-Modulen zusammengesetzt, wie es bei JATS und DocBook der Fall ist, flachen Sie sie zuerst ab.
Die Syntax und die Stellen, an denen man stolpert
Eine DTD kennt vier Deklarationsarten und eine kurze, scharfe Grammatik: Elemente geben ein Inhaltsmodell, Attributlisten geben Typen und Vorgaben, Entities geben Textersetzungen, Notationen benennen externe Formate. Die Form für gemischten Inhalt erwischt die meisten: Ein Element, das Text und Kinder enthält, muss genau als (#PCDATA | kind)* geschrieben werden, mit #PCDATA zuerst und der ganzen Gruppe wiederholbar. (#PCDATA, em)* ist ein Syntaxfehler, keine andere Bedeutung.
- <!ELEMENT catalog (product+)> deklariert Kinder. Operatoren sind , für Folge und | für Auswahl, dazu ? * + für optional, null oder mehr und eins oder mehr; EMPTY und ANY sind die besonderen Modelle.
- <!ATTLIST product id ID #REQUIRED status (draft|live) "draft"> deklariert Attribute. Vorgaben sind #REQUIRED, #IMPLIED, #FIXED "Wert" oder ein literaler Standardwert.
- Attributtypen sind CDATA, die tokenisierten ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN und NMTOKENS sowie Aufzählungen. Ein Element darf höchstens ein ID-Attribut tragen.
- ID-Werte müssen XML-Namen sein, also ist id="1" ungültig und id="n1" in Ordnung. xs:ID im XSD erbt dieselbe Regel.
- <!ENTITY corp "Acme Ltd."> deklariert eine interne allgemeine Entity; SYSTEM und PUBLIC deklarieren externe, und NDATA kennzeichnet eine ungeparste Entity, die eine NOTATION braucht.
- <!ENTITY % common SYSTEM "common.mod"> deklariert eine Parameter-Entity, referenziert als %common;, und das ist der einzige Modularisierungsmechanismus der DTD.
Externe DTDs werden nie geladen, und genau darum geht es
Wenn ein Dokument <!DOCTYPE article SYSTEM "JATS-journalpublishing1.dtd"> sagt, muss ein Validator, der das befolgt, diese Datei holen: eine Anfrage von Ihrem Rechner, in Ihrem Netz, gesteuert von Inhalt, den jemand anderes geschrieben hat. Hier tut das nichts. Jeder Parsevorgang nutzt XML_PARSE_NO_XXE und NONET, und dieser Build registriert keinen Resource-Loader, sodass ein SYSTEM-Bezeichner ins Leere läuft, ob er nun eine URL oder einen Pfad nennt.
Die DTD ist die klassische Angriffsfläche in XML, und deshalb ist das eine Eigenschaft und keine Einschränkung. Externe Entity-Deklarationen in einem DOCTYPE sind das ganze XXE: ein file://-Bezeichner, der in ein Element gelesen wird, eine Intranet-Adresse, die von innerhalb Ihres Perimeters abgetastet wird, ein Out-of-Band-Kanal, gebaut aus einer Parameter-Entity. Verschachtelte Entities sind der Billion-Laughs-Vektor, und libxml2 warnt in der eigenen Dokumentation, dass DTD-Validierung bei nicht vertrauenswürdiger Eingabe niemals eingeschaltet werden sollte. XML_PARSE_HUGE wird ebenfalls nie gesetzt, die Obergrenzen bleiben also stehen: Elementtiefe 256, Entity-Verschachtelung 20, eine Verstärkungsgrenze.
Wo DTDs wirklich noch im Einsatz sind
Die DTD ist die einzige Schemasprache, die in der XML-Spezifikation selbst steckt, also validiert jeder konforme Parser ohne zusätzliche Abhängigkeit gegen eine. Darum überlebt sie dort, wo eine Schemabibliothek hinzuzufügen keine Option ist. Das stärkste lebende Ökosystem ist das wissenschaftliche Publizieren: JATS, der NISO-Standard Z39.96 für Zeitschriftenartikel-XML, hat seine 1.4-DOCTYPEs im November 2024 ergänzt.
- JATS und BITS, im Zeitschriften- und Buchverlag. Ausgeliefert als DTD, RELAX NG und W3C Schema; geprüft wird in den meisten Einlieferungssystemen die DTD.
- Apples Property Lists: Jede plist aus den Werkzeugen von macOS und iOS trägt noch den PUBLIC-Bezeichner von PLIST 1.0.
- DocBook 4.x, in Toolchains, die nie umgestiegen sind. DocBook 5.0 wechselte zu RELAX NG, prüfen Sie also Ihre Version.
- TEI, dessen primäres Schema RELAX NG ist, das aber weiterhin DTD-Ableitungen erzeugt.
- XHTML-Doctypes, heute meist dekorativ, aber in manchen Publikations-Pipelines noch geprüft.
- Ältere SOAP- und EDI-Pipelines, wo eine DTD einmal geschrieben wurde und sich das Format seitdem nicht geändert hat.
Im Code gegen eine DTD validieren
DTD-Validierung stellt die sichere Voreinstellung und die gewünschte Funktion gegeneinander: Der übliche Härtungsrat lautet, DOCTYPE-Deklarationen zu verbieten, und das geht nicht zusammen mit einer Prüfung gegen eine DTD. Jedes Beispiel behält die Deklaration und schaltet stattdessen die externe Auflösung ab.
// 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.Die Beispiele in Java und .NET zeigen dieselbe Spannung von zwei Seiten: Die übliche XXE-Gegenmaßnahme verträgt sich nicht mit DTD-Validierung, die Härtung, die funktioniert, besteht also darin, die Deklaration zu behalten und den Resolver zu entfernen.
Häufige Fragen
Warum lädt es die DTD nicht, auf die mein Dokument verweist?
Weil diese Anfrage von Inhalt gesteuert würde, den Sie womöglich nicht kontrollieren, und weil in der DTD die schlimmsten Sicherheitsprobleme von XML wohnen: eine externe Entity, die eine lokale Datei in ein Element liest, oder die von innerhalb Ihres Netzes eine Intranet-Adresse abtastet. Der sichere Weg, das zu verhindern, ist, gar keinen Resolver zu registrieren.
Der Preis ist ein Handgriff: Holen Sie die DTD selbst und fügen Sie sie ein.
Verlässt irgendetwas, das ich einfüge, den Browser?
Nein. libxml2 läuft als WebAssembly in einem Web Worker dieses Tabs, Dokument und DTD bleiben also beide hier. Es gibt keinen Upload-Endpunkt und keine Analytics mit Zugriff auf den Editor.
Das zählt für die Leute, die noch DTDs benutzen: ein Zeitschriftenartikel unter Sperrfrist, eine plist vom Gerät eines Kunden, eine EDI-Nachricht mit den Preisen eines Partners. Nichts davon gehört in ein Formular-POST.
Kann ich gegen die DocBook-, JATS- oder XHTML-DTD validieren?
Ja, wenn Sie sie als eine einzige abgeflachte Datei haben. Die oberste DTD einer modularen Familie funktioniert nicht: Solche ziehen über Parameter-Entities und SYSTEM-Bezeichner .ent- und .mod-Dateien herein, und keine dieser Referenzen löst hier auf.
Dazu kommt eine Regel der Spezifikation: In einem internen Subset darf eine Parameter-Entity-Referenz als ganze Deklaration stehen, aber nicht innerhalb einer, also ist (%block.mix;) ein Fehler statt einer Expansion. Für diese Familien nehmen Sie lokal xmllint mit --dtdvalid.
Sollte ich eine DTD oder ein XSD verwenden?
Nehmen Sie ein XSD, wenn Sie Datentypen, Namensräume oder eine genauere Kardinalität als optional und wiederholbar brauchen. Eine DTD kann nicht sagen, dass ein Feld eine ganze Zahl zwischen 1 und 99 ist, und sie vergleicht präfigierte Namen wörtlich, sodass ein Dokument scheitert, das denselben Namensraum an ein anderes Präfix bindet.
Nehmen Sie eine DTD, wenn ein System, das Sie beliefern, eine verlangt – das deckt den größten Teil des Verlagswesens ab – oder wenn Sie schätzen, dass sie gar keine Bibliothek braucht: Eine DTD mit 40 Zeilen sagt so viel wie 150 Zeilen XSD.
Warum wird mein Attribut id="1" abgelehnt?
Weil ID-Werte XML-Namen sein müssen und ein Name nicht mit einer Ziffer beginnen darf. Setzen Sie einen Buchstaben oder einen Unterstrich davor, und der Fehler verschwindet.
Zwei verwandte Regeln erwischen viele: Ein Element darf höchstens ein ID-Attribut tragen, und es muss als #REQUIRED oder #IMPLIED deklariert sein, nie mit einem Standardwert. xs:ID im XSD erbt dieselbe Einschränkung.
Ist es sicher, ein Dokument zu validieren, das von jemand anderem kam?
Hier ja, was ungewöhnlich genug ist, um es zu sagen. Die beiden Risiken bei einer DTD sind externe Auflösung und Entity-Expansion, und beide sind geschlossen: Es ist kein Resource-Loader registriert, und XML_PARSE_HUGE wird nie gesetzt, also gelten die Tiefengrenze von 256, die Entity-Verschachtelungsgrenze von 20 und die Verstärkungsgrenze.
Im eigenen Code sollten Sie vorsichtiger sein: Java und ältere PHP-Versionen lösen externe Entities standardmäßig auf.