DTD バリデーター

内部・貼り付けどちらの 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 は、既定で外部実体を解決します。

関連ツール

関連する解説

これで解決できるエラー