Validador DTD
Valide com uma DTD, interna ou colada. Nada é enviado.
Tudo roda nesta aba. Nada do que você colar é enviado, registrado ou transmitido para lugar nenhum. Abra o painel de rede e confira.
Cole um documento com subconjunto interno, as declarações que ficam entre os colchetes do seu DOCTYPE, e clique em Validar contra DTD. Se a DTD estiver num arquivo separado, cole-a no painel da direita. De um jeito ou de outro, o libxml2, compilado para WebAssembly, roda a verificação nesta aba e relata elementos não declarados, violações do modelo de conteúdo e problemas de atributos com linha e coluna.
Esta é a verificação que nenhuma outra ferramenta on-line gratuita oferece: o que aparece ao procurá-la é a página comercial de um fabricante de software de mesa, um site de tutoriais, um trecho de livro da O’Reilly e uma URL da Microsoft com previous-versions no caminho. Enquanto isso, o formato está em uso diário: na edição de periódicos, nas listas de propriedades da Apple, em cadeias de DocBook 4 e numa longa cauda de pipelines EDI.
Um comportamento a conhecer antes de começar: uma DTD externa referenciada por SYSTEM ou PUBLIC nunca é buscada. Isso é deliberado, e o motivo está mais abaixo.
Como uma DTD colada é aplicada
Uma DTD não é um documento separado como é um XSD; ela faz parte do prólogo do documento. Por isso uma DTD colada é enxertada antes da análise: a ferramenta toma a primeira tag de abertura como nome do DOCTYPE, descarta qualquer DOCTYPE já existente, mantém a declaração XML e escreve as suas declarações como subconjunto interno. Reescrever em vez de carregar um subconjunto externo significa que não existe caminho de código capaz de abrir um arquivo ou uma URL.
Isso tem uma consequência, e ela vem da especificação XML, não desta ferramenta. Num subconjunto interno uma referência a entidade de parâmetro pode ficar onde caberia uma declaração de marcação, mas não dentro de uma, então uma DTD que contenha <!ELEMENT article (%block.mix;)> é legítima como subconjunto externo e vira erro quando colada aqui. Se a sua for montada a partir de módulos .ent, como são as de JATS e DocBook, achate-a primeiro.
A sintaxe, e as partes em que as pessoas tropeçam
Uma DTD tem quatro tipos de declaração e uma gramática curta e afiada: elementos dão um modelo de conteúdo, listas de atributos dão tipos e padrões, entidades dão substituições de texto, notações nomeiam formatos externos. A forma de conteúdo misto pega quase todo mundo: um elemento que contém texto e filhos precisa ser escrito exatamente como (#PCDATA | filho)*, com #PCDATA primeiro e o grupo inteiro repetível. (#PCDATA, em)* é erro de sintaxe, não outro significado.
- <!ELEMENT catalog (product+)> declara filhos. Os operadores são , para sequência e | para escolha, com ? * + para opcional, zero ou mais e um ou mais; EMPTY e ANY são os modelos especiais.
- <!ATTLIST product id ID #REQUIRED status (draft|live) "draft"> declara atributos. Os padrões são #REQUIRED, #IMPLIED, #FIXED "valor" ou um valor literal.
- Os tipos de atributo são CDATA, os tokenizados ID, IDREF, IDREFS, ENTITY, ENTITIES, NMTOKEN e NMTOKENS, e as enumerações. Um elemento pode carregar no máximo um atributo ID.
- Valores de ID precisam ser nomes XML, então id="1" é inválido e id="n1" está certo. O xs:ID do XSD herda a mesma regra.
- <!ENTITY corp "Acme Ltd."> declara uma entidade geral interna; SYSTEM e PUBLIC declaram externas, e NDATA marca uma entidade não analisada que precisa de uma NOTATION.
- <!ENTITY % common SYSTEM "common.mod"> declara uma entidade de parâmetro, referenciada como %common;, que é o único mecanismo de modularidade da DTD.
DTDs externas nunca são buscadas, e esse é justamente o ponto
Quando um documento diz <!DOCTYPE article SYSTEM "JATS-journalpublishing1.dtd">, um validador que honre isso precisa ir buscar aquele arquivo: uma requisição a partir da sua máquina, na sua rede, dirigida por conteúdo que outra pessoa escreveu. Aqui nada faz isso. Toda análise usa XML_PARSE_NO_XXE e NONET, e esta compilação não registra nenhum carregador de recursos, então um identificador SYSTEM não resolve para nada, quer nomeie uma URL, quer um caminho.
A DTD é a superfície de ataque clássica do XML, e é por isso que isto é uma propriedade e não uma limitação. Declarações de entidade externa num DOCTYPE são todo o XXE: um identificador file:// lido para dentro de um elemento, um endereço de intranet sondado de dentro do seu perímetro, um canal fora de banda construído com uma entidade de parâmetro. Entidades aninhadas são o vetor do billion laughs, e o próprio libxml2 avisa na sua documentação que a validação por DTD jamais deveria ser habilitada sobre entrada não confiável. XML_PARSE_HUGE também nunca é ligado, então os tetos continuam de pé: profundidade de elementos 256, aninhamento de entidades 20, e um limite de amplificação.
Onde DTDs ainda são realmente usadas
A DTD é a única linguagem de esquema embutida na própria especificação XML, então todo analisador conforme valida contra uma sem nenhuma dependência extra. É por isso que ela sobrevive onde acrescentar uma biblioteca de esquemas não é opção. O ecossistema vivo mais forte é a edição acadêmica: o JATS, o padrão NISO Z39.96 para XML de artigos de periódico, acrescentou seus DOCTYPEs 1.4 em novembro de 2024.
- JATS e BITS, na edição de periódicos e livros. Distribuídos em versões DTD, RELAX NG e W3C Schema; a DTD é o que a maioria dos sistemas de ingestão verifica.
- As listas de propriedades da Apple: todo plist das ferramentas de macOS e iOS ainda carrega o identificador PUBLIC do PLIST 1.0.
- DocBook 4.x, em cadeias que nunca migraram. O DocBook 5.0 passou para RELAX NG, então confira a sua versão.
- TEI, cujo esquema principal é RELAX NG mas que ainda gera derivações em DTD.
- Os doctypes de XHTML, hoje quase decorativos, mas ainda validados em alguns pipelines de publicação.
- Pipelines antigos de SOAP e EDI, onde uma DTD foi escrita uma vez e o formato não mudou desde então.
Validar contra uma DTD em código
A validação por DTD coloca em conflito o padrão seguro e o recurso que você quer: o conselho padrão de endurecimento é proibir declarações DOCTYPE, e não dá para fazer isso e validar contra uma DTD. Cada exemplo mantém a declaração e, em vez disso, desliga a resolução 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.Os exemplos de Java e .NET mostram a mesma tensão por dois lados: a mitigação padrão de XXE é incompatível com a validação por DTD, então o endurecimento que funciona é manter a declaração e remover o resolvedor.
Perguntas frequentes
Por que ele não busca a DTD que meu documento referencia?
Porque essa requisição seria dirigida por conteúdo que talvez você não controle, e a DTD é onde moram os piores problemas de segurança do XML: uma entidade externa que lê um arquivo local para dentro de um elemento, ou que sonda um endereço de intranet de dentro da sua rede. O jeito certo de impedir isso é não registrar resolvedor nenhum.
O custo é um passo manual: busque a DTD você mesmo e cole-a.
Algo que eu colo sai do navegador?
Não. O libxml2 roda como WebAssembly num Web Worker desta aba, então o documento e a DTD ficam os dois aqui. Não há endpoint de upload nem analytics com acesso ao editor.
Isso importa para quem ainda usa DTDs: um artigo de periódico sob embargo, um plist tirado do dispositivo de um cliente, uma mensagem EDI carregando os preços de um parceiro. Nada disso tem lugar num envio de formulário.
Posso validar contra a DTD do DocBook, do JATS ou do XHTML?
Pode, se a tiver como um único arquivo achatado. A DTD de nível mais alto de uma família modular não vai funcionar: essas puxam arquivos .ent e .mod com entidades de parâmetro e identificadores SYSTEM, e nenhuma dessas referências resolve aqui.
Há também uma regra da especificação: num subconjunto interno uma referência a entidade de parâmetro pode figurar como uma declaração inteira, mas não dentro de uma, então (%block.mix;) é erro em vez de expansão. Para essas famílias, use o xmllint com --dtdvalid localmente.
Devo usar uma DTD ou um XSD?
Use um XSD se precisar de tipos de dados, espaços de nomes ou cardinalidade mais precisa que opcional e repetível. Uma DTD não consegue dizer que um campo é um inteiro entre 1 e 99, e ela casa nomes prefixados literais, então um documento que ligue o mesmo espaço de nomes a outro prefixo falha.
Use uma DTD se um sistema que você alimenta exigir uma, o que cobre boa parte da edição, ou se você valoriza que ela não precisa de biblioteca nenhuma: uma DTD de 40 linhas diz tanto quanto 150 linhas de XSD.
Por que meu atributo id="1" é rejeitado?
Porque valores de ID precisam ser nomes XML, e um nome não pode começar com dígito. Ponha uma letra ou um sublinhado na frente do valor e o erro some.
Duas regras aparentadas pegam as pessoas: um elemento pode carregar no máximo um atributo ID, e ele precisa ser declarado #REQUIRED ou #IMPLIED, nunca com um valor padrão. O xs:ID do XSD herda a mesma restrição.
É seguro validar um documento que veio de outra pessoa?
Aqui é, o que é incomum o bastante para valer ser dito. Os dois riscos com uma DTD são a resolução externa e a expansão de entidades, e ambos estão fechados: nenhum carregador de recursos é registrado, e XML_PARSE_HUGE nunca é ligado, então valem o limite de profundidade de 256, o limite de aninhamento de entidades de 20 e o teto de amplificação.
Seja mais cuidadoso no seu próprio código: Java e versões antigas do PHP resolvem entidades externas por padrão.