Validatore DTD

Convalida con una DTD, interna o incollata. Nulla viene caricato.

Input
DTD (lascia vuoto per usare il sottoinsieme interno)
In attesaIncolla un documento per controllarlo. La convalida gira mentre scrivi.

Tutto viene eseguito in questa scheda. Nulla di ciò che incolli viene caricato, registrato o inviato da qualche parte. Apri il pannello di rete e verifica.

Incolla un documento con un sottoinsieme interno, cioè le dichiarazioni fra le parentesi quadre del suo DOCTYPE, e premi Convalida con la DTD. Se la DTD vive in un file a parte, incollala invece nel riquadro di destra. In entrambi i casi libxml2, compilato in WebAssembly, esegue il controllo in questa scheda e segnala elementi non dichiarati, violazioni del modello di contenuto e problemi di attributi con riga e colonna.

Questo è il controllo che nessun altro strumento online gratuito offre: cercandolo si trovano la pagina commerciale di un produttore di software desktop, un sito di tutorial, un estratto di un libro O’Reilly e una URL Microsoft con previous-versions nel percorso. Intanto il formato è in uso quotidiano: nell’editoria delle riviste, nelle property list di Apple, nelle toolchain DocBook 4 e in una lunga coda di pipeline EDI.

Un comportamento da conoscere prima di cominciare: una DTD esterna referenziata con SYSTEM o PUBLIC non viene mai scaricata. È una scelta deliberata, e il motivo è più sotto.

Come viene applicata una DTD incollata

Una DTD non è un documento separato come lo è un XSD; fa parte del prologo del documento. Perciò una DTD incollata viene innestata prima dell’analisi: lo strumento prende il primo tag di apertura come nome del DOCTYPE, scarta qualunque DOCTYPE già presente, conserva la dichiarazione XML e scrive le tue dichiarazioni come sottoinsieme interno. Riscrivere invece di caricare un sottoinsieme esterno significa che non esiste alcun percorso di codice in grado di aprire un file o una URL.

Questo ha una conseguenza, e viene dalla specifica XML, non da questo strumento. In un sottoinsieme interno un riferimento a entità parametro può stare dove potrebbe stare una dichiarazione di markup, ma non dentro una dichiarazione: una DTD che contenga <!ELEMENT article (%block.mix;)> è quindi lecita come sottoinsieme esterno e diventa un errore se incollata qui. Se la tua è assemblata da moduli .ent, come lo sono quelle di JATS e DocBook, appiattiscila prima.

La sintassi, e i punti su cui si inciampa

Una DTD ha quattro tipi di dichiarazione e una grammatica breve e affilata: gli elementi danno un modello di contenuto, le liste di attributi danno tipi e valori predefiniti, le entità danno sostituzioni di testo, le notazioni danno un nome a formati esterni. La forma del contenuto misto frega quasi tutti: un elemento che contiene sia testo sia figli va scritto esattamente come (#PCDATA | figlio)*, con #PCDATA per primo e l’intero gruppo ripetibile. (#PCDATA, em)* è un errore di sintassi, non un significato diverso.

  • <!ELEMENT catalog (product+)> dichiara i figli. Gli operatori sono , per la sequenza e | per la scelta, con ? * + per facoltativo, zero o più e uno o più; EMPTY e ANY sono i modelli speciali.
  • <!ATTLIST product id ID #REQUIRED status (draft|live) "draft"> dichiara gli attributi. I predefiniti sono #REQUIRED, #IMPLIED, #FIXED "valore", oppure un valore letterale.
  • I tipi di attributo sono CDATA, i tipi a token ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN e NMTOKENS, e le enumerazioni. Un elemento può portare al massimo un attributo ID.
  • I valori ID devono essere nomi XML, quindi id="1" non è valido e id="n1" va bene. xs:ID in XSD eredita la stessa regola.
  • <!ENTITY corp "Acme Ltd."> dichiara un’entità generale interna; SYSTEM e PUBLIC ne dichiarano di esterne, e NDATA marca un’entità non analizzata che richiede una NOTATION.
  • <!ENTITY % common SYSTEM "common.mod"> dichiara un’entità parametro, referenziata come %common;, che è l’unico meccanismo di modularità della DTD.

Le DTD esterne non vengono mai scaricate, ed è proprio questo il punto

Quando un documento dice <!DOCTYPE article SYSTEM "JATS-journalpublishing1.dtd">, un validatore che onori quel riferimento deve andare a prendere quel file: una richiesta dalla tua macchina, sulla tua rete, guidata da contenuto scritto da qualcun altro. Qui non lo fa nulla. Ogni analisi usa XML_PARSE_NO_XXE e NONET, e questa build non registra alcun caricatore di risorse, quindi un identificatore SYSTEM non si risolve in niente, che nomini una URL o un percorso.

La DTD è la classica superficie d’attacco di XML, ed è per questo che qui si tratta di una proprietà e non di un limite. Le dichiarazioni di entità esterna in un DOCTYPE sono tutto l’XXE: un identificatore file:// letto dentro un elemento, un indirizzo di intranet sondato da dentro il tuo perimetro, un canale fuori banda costruito con un’entità parametro. Le entità annidate sono il vettore del billion laughs, e libxml2 avverte nella propria documentazione che la convalida DTD non andrebbe mai abilitata su input non fidato. Nemmeno XML_PARSE_HUGE viene mai impostato, quindi i tetti restano: profondità degli elementi 256, annidamento delle entità 20, e un limite di amplificazione.

Dove le DTD sono ancora davvero in uso

La DTD è l’unico linguaggio di schema incorporato nella specifica XML stessa, quindi ogni parser conforme convalida con una DTD senza alcuna dipendenza aggiuntiva. È per questo che sopravvive là dove aggiungere una libreria di schemi non è un’opzione. L’ecosistema vivo più solido è l’editoria accademica: JATS, lo standard NISO Z39.96 per l’XML degli articoli di rivista, ha aggiunto i suoi DOCTYPE 1.4 nel novembre 2024.

  • JATS e BITS, nell’editoria di riviste e libri. Distribuiti in versione DTD, RELAX NG e W3C Schema; la DTD è ciò che controllano quasi tutti i sistemi di acquisizione.
  • Le property list di Apple: ogni plist prodotto dagli strumenti di macOS e iOS porta ancora l’identificatore PUBLIC di PLIST 1.0.
  • DocBook 4.x, nelle toolchain che non si sono mai mosse. DocBook 5.0 è passato a RELAX NG, quindi controlla la tua versione.
  • TEI, il cui schema principale è RELAX NG ma che genera ancora derivazioni in DTD.
  • I doctype XHTML, oggi perlopiù decorativi ma ancora convalidati in alcune pipeline editoriali.
  • Vecchie pipeline SOAP ed EDI, dove una DTD è stata scritta una volta e il formato da allora non è cambiato.

Convalidare con una DTD da codice

La convalida DTD mette in conflitto l’impostazione sicura predefinita e la funzione che vuoi: il consiglio standard di irrobustimento è vietare le dichiarazioni DOCTYPE, e non puoi farlo e insieme convalidare con una DTD. Ogni esempio conserva la dichiarazione e spegne invece la risoluzione esterna.

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

Gli esempi in Java e .NET mostrano la stessa tensione da due lati: la mitigazione XXE standard è incompatibile con la convalida DTD, quindi l’irrobustimento che funziona consiste nel tenere la dichiarazione e togliere il resolver.

Domande frequenti

Perché non scarica la DTD a cui il mio documento fa riferimento?

Perché quella richiesta sarebbe guidata da contenuto che potresti non controllare, e la DTD è il luogo dove vivono i peggiori problemi di sicurezza di XML: un’entità esterna che legge un file locale dentro un elemento, o che sonda un indirizzo di intranet da dentro la tua rete. Il modo sicuro di impedirlo è non registrare alcun resolver.

Il costo è un passaggio manuale: scarica la DTD tu e incollala.

Quello che incollo esce dal browser?

No. libxml2 gira come WebAssembly in un Web Worker di questa scheda, quindi il documento e la DTD restano entrambi qui. Non c’è alcun endpoint di caricamento né analytics con accesso all’editor.

Conta per chi usa ancora le DTD: un articolo di rivista sotto embargo, un plist estratto dal dispositivo di un cliente, un messaggio EDI che porta i prezzi di un partner. Nessuna di queste cose sta bene dentro un invio di form.

Posso convalidare con la DTD di DocBook, JATS o XHTML?

Sì, se ce l’hai come un unico file appiattito. La DTD di livello superiore di una famiglia modulare non funzionerà: quelle tirano dentro file .ent e .mod con entità parametro e identificatori SYSTEM, e nessuno di quei riferimenti si risolve qui.

C’è anche una regola della specifica: in un sottoinsieme interno un riferimento a entità parametro può valere come dichiarazione intera ma non stare dentro una dichiarazione, quindi (%block.mix;) è un errore e non un’espansione. Per queste famiglie usa xmllint con --dtdvalid in locale.

Dovrei usare una DTD o un XSD?

Usa un XSD se ti servono tipi di dato, namespace, o una cardinalità più precisa di facoltativo e ripetibile. Una DTD non può dire che un campo è un intero fra 1 e 99, e confronta letteralmente i nomi con prefisso, quindi un documento che lega lo stesso namespace a un prefisso diverso fallisce.

Usa una DTD se un sistema che alimenti ne richiede una, il che copre gran parte dell’editoria, oppure se apprezzi il fatto che non richieda alcuna libreria: una DTD di 40 righe dice quanto 150 righe di XSD.

Perché il mio attributo id="1" viene rifiutato?

Perché i valori ID devono essere nomi XML, e un nome non può iniziare con una cifra. Anteponi al valore una lettera o un trattino basso e l’errore sparisce.

Due regole imparentate fregano molti: un elemento può portare al massimo un attributo ID, e dev’essere dichiarato #REQUIRED o #IMPLIED, mai con un valore predefinito. xs:ID in XSD eredita lo stesso vincolo.

È sicuro convalidare un documento arrivato da qualcun altro?

Qui sì, cosa abbastanza insolita da meritare di essere detta. I due rischi con una DTD sono la risoluzione esterna e l’espansione delle entità, ed entrambi sono chiusi: non è registrato alcun caricatore di risorse, e XML_PARSE_HUGE non viene mai impostato, quindi valgono il limite di profondità di 256, quello di annidamento delle entità di 20 e il tetto di amplificazione.

Nel tuo codice sii più prudente: Java e le vecchie versioni di PHP risolvono le entità esterne per impostazione predefinita.

Strumenti correlati

Approfondimenti

Errori che risolve