[PHP]xml_set_notation_decl_handlerとは?DTDの記法宣言を検知する方法を徹底解説

PHP

はじめに

前回の記事では、XXE攻撃対策として重要な xml_set_external_entity_ref_handler() を解説しました。今回取り上げる xml_set_notation_decl_handler() も同じくDTD(文書型定義)に関連するハンドラですが、その役割は「記法宣言(Notation Declaration)」という、やや専門的でマイナーな概念を検知することにあります。

「記法宣言」とは、XML文書内で非XML形式のデータ(画像ファイルなど)を扱う際に、そのデータ形式を識別するための仕組みです。現代のWeb開発において目にする機会は非常に少なくなりましたが、古いDTDベースのXML文書や、特定の業界標準フォーマットとの互換性が必要な場面では、今でも登場することがあります。本記事では、この関数の役割と、実践的な活用パターン、そして現代における位置づけを詳しく解説します。


関数概要

項目内容
関数名xml_set_notation_decl_handler()
所属拡張XML Parser拡張(Expatベース、標準で有効)
シグネチャxml_set_notation_decl_handler(XMLParser $parser, ?callable $handler): true
引数1$parserxml_parser_create()等で生成したパーサーインスタンス
引数2$handler — 記法宣言検知時に呼び出すコールバック(nullで解除)
コールバックの引数(XMLParser $parser, string $notationName, string $base, string $systemId, string $publicId)
戻り値常に true
対応バージョンPHP 4以降
用途DTD内の<!NOTATION ...>宣言の検知

記法宣言の全体像(イメージ図)

  DTDを含むXML文書
  <!DOCTYPE root [
    <!NOTATION jpeg SYSTEM "image/jpeg">
    <!ENTITY logo SYSTEM "logo.jpg" NDATA jpeg>
  ]>
  <root>...</root>
              │
              ▼
        xml_parse()による解析
              │
              │ "<!NOTATION jpeg SYSTEM ...>" を検知
              ▼
  ┌───────────────────────────────┐
  │ xml_set_notation_decl_handler()      │ ★この記事の対象
  │  → notationName="jpeg"               │
  │    systemId="image/jpeg"             │
  │    のような情報がコールバックに渡される  │
  └───────────────────────────────┘

ポイントは、記法宣言(<!NOTATION>)が**「この名前のデータ形式は、このシステムIDで識別される外部形式である」**ということをDTD内で定義するための仕組みだという点です。これは主に、XML文書内で画像やその他のバイナリデータを表す「非解析対象実体(unparsed entity)」と組み合わせて使われます。


実践サンプル7選

例1:基本的な使い方

<?php

class BasicNotationDemo
{
    public function detectNotations(string $xml): array
    {
        $notations = [];
        $parser = xml_parser_create();

        xml_set_notation_decl_handler(
            $parser,
            function ($p, $notationName, $base, $systemId, $publicId) use (&$notations) {
                $notations[] = [
                    'name'      => $notationName,
                    'system_id' => $systemId,
                    'public_id' => $publicId,
                ];
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $notations;
    }
}

$demo = new BasicNotationDemo();
$xml = '<!DOCTYPE root [<!NOTATION gif SYSTEM "image/gif">]><root/>';
print_r($demo->detectNotations($xml));

例2:検知した記法をログに記録するデバッグツール

<?php

class NotationDeclarationLogger
{
    /**
     * DTD内の記法宣言の情報をログとして記録する
     * (レガシーなXMLフォーマットのデバッグに有用)
     */
    public function logNotations(string $xml, string $logFilePath): void
    {
        $parser = xml_parser_create();

        xml_set_notation_decl_handler(
            $parser,
            function ($p, $notationName, $base, $systemId, $publicId) use ($logFilePath) {
                $entry = sprintf(
                    "[%s] notation=%s system=%s public=%s\n",
                    date('Y-m-d H:i:s'),
                    $notationName,
                    $systemId,
                    $publicId
                );
                file_put_contents($logFilePath, $entry, FILE_APPEND);
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);
    }
}

$logger = new NotationDeclarationLogger();
$logger->logNotations(
    '<!DOCTYPE root [<!NOTATION jpeg SYSTEM "image/jpeg">]><root/>',
    '/tmp/notation_log.txt'
);

例3:許可された記法のみを検証するバリデーター

<?php

class AllowedNotationValidator
{
    private array $allowedNotations;

    public function __construct(array $allowedNotations)
    {
        $this->allowedNotations = $allowedNotations;
    }

    /**
     * 文書内で宣言されている記法が、
     * あらかじめ許可したリストに含まれるかどうかを検証する
     */
    public function validate(string $xml): array
    {
        $violations = [];
        $parser = xml_parser_create();

        xml_set_notation_decl_handler(
            $parser,
            function ($p, $notationName) use (&$violations) {
                if (!in_array($notationName, $this->allowedNotations, true)) {
                    $violations[] = "許可されていない記法です: {$notationName}";
                }
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $violations;
    }
}

$validator = new AllowedNotationValidator(['jpeg', 'png']);
print_r($validator->validate('<!DOCTYPE root [<!NOTATION exe SYSTEM "application/exe">]><root/>'));

例4:記法宣言の存在自体をセキュリティ観点で検知するツール

<?php

class LegacyFeatureDetector
{
    /**
     * 記法宣言のような古いDTD機能が使われていること自体を検知し、
     * モダンなXML処理への移行を促すための警告を出す
     */
    public function detectLegacyUsage(string $xml): bool
    {
        $found = false;
        $parser = xml_parser_create();

        xml_set_notation_decl_handler(
            $parser,
            function () use (&$found) {
                $found = true;
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $found;
    }
}

$detector = new LegacyFeatureDetector();
if ($detector->detectLegacyUsage('<!DOCTYPE root [<!NOTATION x SYSTEM "y">]><root/>')) {
    echo '警告: レガシーなDTD記法宣言が使用されています。移行を検討してください。' . PHP_EOL;
}

例5:PUBLIC識別子とSYSTEM識別子の両方を扱うクラス

<?php

class NotationIdentifierExtractor
{
    /**
     * 記法宣言はPUBLIC識別子とSYSTEM識別子のいずれか、
     * または両方を持つ場合があるため、それぞれの有無を確認する
     */
    public function extractDetails(string $xml): array
    {
        $details = [];
        $parser = xml_parser_create();

        xml_set_notation_decl_handler(
            $parser,
            function ($p, $notationName, $base, $systemId, $publicId) use (&$details) {
                $details[] = [
                    'name'        => $notationName,
                    'has_system'  => $systemId !== '',
                    'has_public'  => $publicId !== '',
                    'system_id'   => $systemId,
                    'public_id'   => $publicId,
                ];
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $details;
    }
}

$extractor = new NotationIdentifierExtractor();
$xml = '<!DOCTYPE root [<!NOTATION tex PUBLIC "-//TeX//NOTATION TeX//EN">]><root/>';
print_r($extractor->extractDetails($xml));

例6:記法宣言と非解析対象実体(unparsed entity)の関連を確認する

<?php

class NotationEntityRelationDemo
{
    /**
     * 記法宣言(NOTATION)と、それを参照する実体宣言(ENTITY ... NDATA)の
     * 組み合わせを確認する(実体宣言自体は別のハンドラの対象)
     */
    public function demonstrate(string $xml): array
    {
        $notations = [];
        $parser = xml_parser_create();

        xml_set_notation_decl_handler(
            $parser,
            function ($p, $notationName, $base, $systemId) use (&$notations) {
                $notations[$notationName] = $systemId;
            }
        );

        xml_parse($parser, $xml, true);
        xml_parser_free($parser);

        return $notations;
    }
}

$demo = new NotationEntityRelationDemo();
$xml = '<!DOCTYPE root [
  <!NOTATION jpeg SYSTEM "image/jpeg">
  <!ENTITY logo SYSTEM "logo.jpg" NDATA jpeg>
]><root/>';
print_r($demo->demonstrate($xml));

例7:現代的な代替アプローチ(記法宣言に依存しない設計)

<?php

class ModernBinaryDataApproach
{
    /**
     * 現代のシステムでは、記法宣言を使ったバイナリデータの識別ではなく、
     * Base64エンコードやMIMEタイプの明示的な属性指定が一般的
     */
    public function buildModernXml(string $imagePath, string $mimeType): string
    {
        $data = base64_encode(file_get_contents($imagePath));

        $xml = new SimpleXMLElement('<root/>');
        $image = $xml->addChild('image', $data);
        $image->addAttribute('mime-type', $mimeType);
        $image->addAttribute('encoding', 'base64');

        return $xml->asXML();
    }
}

// $approach = new ModernBinaryDataApproach();
// echo $approach->buildModernXml('/path/to/logo.jpg', 'image/jpeg');

関連関数との比較

関数役割xml_set_notation_decl_handlerとの違い
xml_set_notation_decl_handler()DTDの記法宣言(<!NOTATION>)を検知本記事の対象。データ形式の識別子を扱う
xml_set_external_entity_ref_handler()外部実体参照を検知・制御記法とセットで使われる非解析対象実体の「参照」を扱う
xml_set_unparsed_entity_decl_handler()非解析対象実体宣言(NDATA付きENTITY)を検知記法を「使う側」の宣言を検知する、対になる関数
xml_set_default_handler()未処理データ全般を捕捉する記法宣言に特化しておらず、より汎用的
DOMDocument::notationsDOM方式で記法情報にアクセスするSAX方式ではなくDOMツリー経由でのアクセス

よくある落とし穴(注意点)

  1. 現代のXML文書ではほとんど使われない機能である 記法宣言はXML 1.0の古い仕様に由来する機能であり、現代のWeb API開発などではほとんど登場しません。この関数を使う機会があるとすれば、古い業界標準フォーマットとの互換性維持など、限定的な場面になるでしょう。
  2. xml_set_unparsed_entity_decl_handler() との役割分担を混同する 記法「そのものの定義」を検知するのがこの関数であり、その記法を「使用する実体宣言」を検知するのは別のハンドラ(xml_set_unparsed_entity_decl_handler())です。両者はセットで登場することが多いため、役割を混同しないようにしましょう(例6を参照)。
  3. PUBLIC識別子とSYSTEM識別子のどちらが使われるかは文書による 記法宣言は SYSTEM 識別子のみ、PUBLIC 識別子のみ、あるいは両方を持つ場合があります。どちらか一方の存在を前提としたコードを書くと、他のケースで正しく動作しない可能性があります(例5を参照)。
  4. セキュリティ上の直接的なリスクはxml_set_external_entity_ref_handler()ほど高くない 記法宣言自体は外部リソースへのアクセスを直接引き起こすものではありませんが、非解析対象実体と組み合わさることで外部ファイルへの参照が発生することがあります。総合的なXML処理のセキュリティ対策としては、外部実体参照の制御の方がより重要度が高い点を理解しておきましょう。
  5. モダンなAPI(DOMDocument等)では代替の抽象化がなされている DOM方式のAPIでは、記法情報は DOMDocumentType::notations のようなプロパティを通じてよりオブジェクト指向的にアクセスできます。SAX方式のイベント処理に強くこだわる理由がなければ、こうしたモダンなAPIの利用も検討する価値があります。

まとめ

観点まとめ
何をする関数かDTD内の記法宣言(<!NOTATION>)を検知するコールバックを登録する
主な用途レガシーなDTDベースのXML文書における、非XML形式データの識別情報の把握
現代における位置づけ使用機会は非常に限定的。主に古いフォーマットとの互換性維持が目的
関連する仕組み非解析対象実体(NDATA付きENTITY)とセットで使われることが多い
注意点役割の似たxml_set_unparsed_entity_decl_handler()との混同、PUBLIC/SYSTEM識別子の有無、モダンAPIとの比較検討

xml_set_notation_decl_handler() は、XML 1.0の歴史的な機能である記法宣言を扱うための、やや専門性の高い関数です。現代の開発で新たに採用する機会は少ないものの、レガシーシステムとの連携やDTDベースの古いXML文書を扱う際には、その存在と役割を理解しておくことが役立ちます。

タイトルとURLをコピーしました