DTD バリデーター
内部・貼り付けどちらの DTD でも検証。アップロードなし。
すべてこのタブ内で実行されます。貼り付けた内容がアップロード・記録・送信されることはありません。 ネットワークパネルを開いて確認する.
内部サブセット、つまり DOCTYPE の角かっこの中にある宣言を含む文書を貼り付けて、「DTD で検証」を押してください。DTD が別ファイルにあるなら、右側のペインに貼り付けてください。どちらの場合も、WebAssembly にコンパイルされた libxml2 がこのタブの中で検査を走らせ、未宣言の要素、内容モデル違反、属性の問題を行と列つきで報告します。
これは他の無料オンラインツールが提供していない検査です。検索して出てくるのは、デスクトップ製品の販売ページ、チュートリアルサイト、O’Reilly の書籍の抜粋、そしてパスに previous-versions が入った Microsoft の URL です。その一方でこの形式は日々使われています。学術誌の出版、Apple のプロパティリスト、DocBook 4 のツールチェーン、そして長い裾を引く EDI のパイプラインで。
始める前に知っておくべき挙動が 1 つあります。SYSTEM や PUBLIC で参照された外部 DTD は、決して取得しません。これは意図的なもので、理由は下に書いてあります。
貼り付けた DTD はどう適用されるか
DTD は XSD のような独立した文書ではなく、文書のプロログの一部です。ですから貼り付けた DTD は、解析の前に差し込まれます。ツールは最初の開始タグを DOCTYPE 名として採り、すでにある DOCTYPE は捨て、XML 宣言は残し、あなたの宣言を内部サブセットとして書き込みます。外部サブセットを読み込むのではなく書き換えるので、ファイルや URL を開きうるコード経路がそもそも存在しません。
これには 1 つ帰結があり、それはこのツールではなく XML 仕様から来ています。内部サブセットでは、パラメータ実体参照はマークアップ宣言が置ける場所には置けますが、宣言の内部には置けません。ですから <!ELEMENT article (%block.mix;)> を含む DTD は外部サブセットとしては正当ですが、ここに貼り付けるとエラーになります。JATS や DocBook のように .ent モジュールから組み立てている DTD なら、先に平坦化してください。
構文と、人がつまずくところ
DTD には宣言が 4 種類あり、短く鋭い文法を持ちます。要素宣言は内容モデルを与え、属性リスト宣言は型と既定値を与え、実体宣言はテキストの置換を与え、記法宣言は外部形式に名前を与えます。ほとんどの人が引っかかるのは混合内容の書き方です。テキストと子要素の両方を持つ要素は、ちょうど (#PCDATA | child)* の形で、#PCDATA を先頭にし、グループ全体を反復可能にして書かなければなりません。(#PCDATA, em)* は別の意味ではなく、構文エラーです。
- <!ELEMENT catalog (product+)> は子要素を宣言します。演算子は連接の , と選択の |、そして省略可・0 回以上・1 回以上の ? * +。EMPTY と ANY が特別な内容モデルです。
- <!ATTLIST product id ID #REQUIRED status (draft|live) "draft"> は属性を宣言します。既定値は #REQUIRED、#IMPLIED、#FIXED "値"、またはリテラルの既定値です。
- 属性型は CDATA、トークン化型の ID、IDREF、IDREFS、ENTITY、ENTITIES、NMTOKEN、NMTOKENS、そして列挙です。1 つの要素が持てる ID 属性は最大 1 つです。
- ID 値は XML 名でなければならないので、id="1" は無効で id="n1" は有効です。XSD の xs:ID も同じ規則を受け継ぎます。
- <!ENTITY corp "Acme Ltd."> は内部一般実体を宣言します。SYSTEM と PUBLIC は外部実体を宣言し、NDATA は NOTATION を必要とする解析対象外実体を示します。
- <!ENTITY % common SYSTEM "common.mod"> はパラメータ実体を宣言し、%common; として参照します。これが DTD にとって唯一のモジュール化の手段です。
外部 DTD を取得しないこと、それこそが要点
文書に <!DOCTYPE article SYSTEM "JATS-journalpublishing1.dtd"> と書いてあるとき、それを尊重するバリデーターはそのファイルを取りに行かなければなりません。あなたのマシンから、あなたのネットワークで、他人が書いた内容に導かれた要求です。ここではそれを何もしません。すべての解析は XML_PARSE_NO_XXE と NONET を使い、このビルドはリソースローダーを一切登録しないので、SYSTEM 識別子は URL でもパスでも何にも解決しません。
DTD は XML における古典的な攻撃面です。だからこれは制限ではなく性質です。DOCTYPE の中の外部実体宣言は、XXE のすべてです。file:// 識別子を要素に読み込む、境界の内側からイントラネットのアドレスを探る、パラメータ実体で帯域外のチャネルを作る。入れ子になった実体は billion laughs の手口であり、libxml2 は自らの文書で、信頼できない入力に対して DTD 検証を有効にしてはならないと警告しています。XML_PARSE_HUGE も決して設定しないので、上限は外れません。要素の深さ 256、実体の入れ子 20、そして増幅量の上限。
DTD が今も本当に使われている場所
DTD は XML 仕様そのものに組み込まれた唯一のスキーマ言語なので、適合するパーサーはどれも追加の依存なしに DTD 検証ができます。だからこそ、スキーマライブラリを足すという選択肢がない場所で生き残っています。今もっとも元気な生態系は学術出版です。学術誌記事 XML の NISO Z39.96 標準である JATS は、2024 年 11 月に 1.4 の DOCTYPE を追加しました。
- JATS と BITS、学術誌と書籍の出版で。DTD、RELAX NG、W3C Schema の各版が配布されていますが、取り込みシステムの多くが検査するのは DTD です。
- Apple のプロパティリスト。macOS と iOS のツールが出すあらゆる plist は、今も PLIST 1.0 の PUBLIC 識別子を持っています。
- DocBook 4.x、移行しなかったツールチェーンで。DocBook 5.0 は RELAX NG に切り替えたので、自分のバージョンを確かめてください。
- TEI。主たるスキーマは RELAX NG ですが、今も DTD 派生物を生成しています。
- XHTML の doctype。今ではほぼ飾りですが、一部の出版パイプラインではまだ検証されています。
- 古い SOAP と EDI のパイプライン。DTD が一度書かれ、それ以来形式が変わっていないところです。
コードで DTD 検証を行う
DTD 検証では、安全な既定値と欲しい機能が衝突します。標準的な堅牢化の助言は DOCTYPE 宣言を禁止することですが、それをしながら DTD 検証はできません。各サンプルは宣言を残したうえで、代わりに外部解決を切ります。
// 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.Java と .NET のサンプルは同じ緊張を二方向から見せます。標準的な XXE 対策は DTD 検証と両立しないので、実際に効く堅牢化は、宣言は残してリゾルバーを取り除くことです。
よくある質問
文書が参照している DTD を取得してくれないのはなぜですか。
その要求は、あなたが制御していないかもしれない内容に導かれるからです。そして DTD は XML で最悪のセキュリティ問題が住む場所です。ローカルファイルを要素に読み込む外部実体、ネットワークの内側からイントラネットのアドレスを探る外部実体。確実に防ぐ方法は、リゾルバーをまったく登録しないことです。
代償は手作業 1 つ分です。DTD を自分で取得して貼り付けてください。
貼り付けたものはブラウザーの外に出ますか。
いいえ。libxml2 はこのタブの Web Worker 内で WebAssembly として動くので、文書も DTD もここに留まります。アップロード先もなく、エディターにアクセスできる解析ツールもありません。
これは今も DTD を使う人たちにとって大事なことです。解禁前の学術論文、顧客の端末から取り出した plist、取引先の価格を含む EDI メッセージ。どれもフォーム送信に載せるものではありません。
DocBook や JATS、XHTML の DTD で検証できますか。
平坦化した 1 つのファイルとして持っているなら、できます。モジュール構成のファミリーの最上位 DTD ではうまくいきません。それらはパラメータ実体と SYSTEM 識別子で .ent や .mod ファイルを取り込みますが、そうした参照はここでは一切解決しません。
仕様上の規則もあります。内部サブセットでは、パラメータ実体参照は宣言 1 つ丸ごととしては置けますが宣言の内部には置けないので、(%block.mix;) は展開ではなくエラーになります。こうしたファミリーには、ローカルで xmllint の --dtdvalid を使ってください。
DTD と XSD、どちらを使うべきですか。
データ型、名前空間、あるいは「省略可」と「反復可」より細かい出現数が必要なら XSD です。DTD はある項目が 1 から 99 の整数だとは言えませんし、接頭辞つきの名前をリテラルに照合するので、同じ名前空間を別の接頭辞に結びつけた文書は通りません。
送り先のシステムが DTD を要求するなら DTD です。出版の世界のほとんどがそれに当たります。ライブラリがまったく不要という点を買うのもよいでしょう。40 行の DTD は、150 行の XSD と同じだけのことを言えます。
なぜ id="1" という属性が拒否されるのですか。
ID 値は XML 名でなければならず、名前は数字で始められないからです。値の前に文字かアンダースコアを付ければエラーは消えます。
関連する規則が 2 つ、人を引っかけます。1 つの要素が持てる ID 属性は最大 1 つであること、そしてそれは #REQUIRED か #IMPLIED で宣言しなければならず、既定値を付けてはならないこと。XSD の xs:ID も同じ制約を受け継ぎます。
他人から受け取った文書を検証しても安全ですか。
ここでは安全です。そう言えること自体が珍しいので、わざわざ書いておきます。DTD における危険は外部解決と実体展開の 2 つで、どちらも閉じています。リソースローダーは登録していませんし、XML_PARSE_HUGE は決して設定しないので、深さ上限 256、実体の入れ子上限 20、増幅量の上限がすべて効きます。
自分のコードではもっと気をつけてください。Java と古い PHP は、既定で外部実体を解決します。