【実務・中級編】 XML外部エンティティ(XXE)攻撃による機密ファイル読み取り – オフェンシブセキュリティ & リバースエンジニアリング防御ガイド

おい、最近のソースコードレビューやペネトレーションテストで見かけるレガシーな脆弱性について、少し話をしようか。

Webアプリケーションのセキュリティにおいて、SQLインジェクションやクロスサイトスクリプティング(XSS)はすっかりお馴染みの顔になったが、XML外部エンティティ(XXE)攻撃の脅威を甘く見ているエンジニアがまだまだ多すぎる。「うちはXMLなんて直接扱ってないから大丈夫」という開発者に限って、裏でSAML認証を導入していたり、ExcelやWordといったOfficeファイルをパースするライブラリをそのまま組み込んでいたりするんだよな。

レッドチームの視点から言えば、XXEは「宝の山」への直通トンネルだ。今回は、このXXEがなぜ危険なのか、実際の攻撃者がどうやってシステムをハッキングするのか、そしてそれを現場でどう完全に封じ込めるのかを、実務に即したコードと設定とともに徹底的に叩き込んでいく。

—

1. なぜXXEは今でも恐ろしいのか?(攻撃のメカニズム)

XML(Extensible Markup Language)には、文書内で動的に値を定義・展開するための「エンティティ」という仕組みが存在する。この仕様自体は正規のものなのだが、XMLパーサーの設定が不適切な場合、外部から指定されたURI(ファイルパスやURL)の内容をそのまま読み込んで処理してしまう。

これがXXE(XML External Entity)だ。

攻撃者が狙うのは、主に以下の2つのシナリオだ。

1. ローカルファイルの機密読み取り: /etc/passwd や /etc/hosts、あるいはデータベースの接続文字列が書かれた設定ファイル、さらにはアプリケーションのソースコードそのものを引き抜く。
2. SSRF(サーバーサイドリクエストフォージェリー): 攻撃者が指定した内部ネットワークのIPアドレスやクラウドのメタデータサービス(AWSの 169.254.169.254 など)にHTTPリクエストを強制発報させ、IAMクレデンシャルを窃取する。

「APIへの入力値バリデーションを厳しくしているから平気だ」という言い訳は通用しない。なぜなら、問題は入力値の形ではなく、「パーサーが外部エンティティの解決を許可しているというサーバー側の設定ミス」にあるからだ。

—

2. 攻撃シミュレーション:PoC(概念実証)の現実

インシデントレスポンスの現場で調査をしていると、攻撃者は以下のような洗練されていない、しかし非常に効果的なペイロードをPOSTリクエストに紛れ込ませてくる。

ターゲットのWebアプリが、受け取ったXMLをそのまま DOMDocument や SimpleXMLElement でパースしていると仮定しよう。攻撃者は次のようなXMLを送信する。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<request>
  <username>&xxe;</username>
  <password>dummy</password>
</request>

XMLパーサーがこのリクエストを受け取った瞬間、内部で <!ENTITY xxe SYSTEM "file:///etc/passwd"> が評価され、サーバー内の /etc/passwd の中身が &xxe; の部分に代入される。運が良ければ、そのレスポンスがそのまま画面に反射するか、エラーメッセージとして親切に画面上に吐き出されることになる。

「うちの環境はLinuxじゃないから /etc/passwd はないよ」なんて笑っている場合じゃない。Windows環境であれば file:///C:/Windows/win.ini が狙われるし、前述の通りクラウド環境ならメタデータエンドポイントへのリクエストによってインフラ全体が陥落する。

—

3. 完全防御:セキュアなXMLパーサーの実装コード

さて、ここからが本題だ。開発チームの君たちが今日から直ちにrepositoryにコミットすべき、セキュアな実装コードを言語別に提示する。

PHPの場合 (libxml_disable_entity_loader)

PHPでXMLを扱う場合、歴史的経緯もありデフォルトでは外部エンティティの読み込みが無効化されていないケースがある。必ず明示的に無効化し、かつ安全なフラグを立てる必要がある。

<?php
/**
 * セキュアなXMLパーサー実装サンプル (PHP)
 * 外部エンティティの解決とDTDの読み込みを完全に遮断する
 */

// 外部エンティティのローダーを無効化(PHP 8.0未満では必須)
if (function_exists('libxml_disable_entity_loader')) {
    libxml_disable_entity_loader(true);
}

// 外部からのDTD読み込みやエンティティ展開を禁止するフラグを設定
$options = LIBXML_NONET | LIBXML_NOENT | LIBXML_DTDLOAD;

// ユーザーからの入力を安全にパースするラッパー関数
function safe_xml_parse(string $xml_content) {
    // エラーハンドリングを有効化し、内部エラーを隠蔽する設定
    libxml_use_internal_errors(true);
    
    // DOMDocumentを使用して安全に読み込み
    $dom = new DOMDocument();
    
    // 外部エンティティの自動解決をブロックする定数を指定してロード
    // LIBXML_NONET はネットワークアクセスを完全に遮断する
    $result = $dom->loadXML($xml_content, LIBXML_NONET);
    
    if ($result === false) {
        // パースエラー時の処理(詳細なシステムエラーを外部に出さないこと)
        $errors = libxml_get_errors();
        libxml_clear_errors();
        throw new \Exception("不正なXMLデータ構造です。");
    }
    
    return $dom;
}

// 使用例
try {
    $userInput = $_POST['xml_data'] ?? '';
    $xmlDoc = safe_xml_parse($userInput);
    // 以降の安全な処理...
} catch (\Exception $e) {
    // ログに詳細を記録し、ユーザーには汎用的なメッセージを返す
    error_log($e->getMessage());
    http_response_code(400);
    echo json_encode(["error" => "リクエストの処理に失敗しました。"]);
}

Pythonの場合 (defusedxml の活用)

Pythonで標準の xml.etree.ElementTree や minidom を使うのは、セキュリティの観点から自殺行為に近い。脆弱性対策が組み込まれたサードパーティライブラリである defusedxml を必ず採用してほしい。

"""
セキュアなXMLパーサー実装サンプル (Python)
defusedxmlライブラリを使用してXXEおよびBillion Laughs攻撃を防御する
"""

# 標準のxmlモジュールの代わりにdefusedxmlをインポートする
# 事前に `pip install defusedxml` が必要
from defusedxml import ElementTree as DefusedET
import logging

def parse_secure_xml(xml_string: str):
    try:
        # defusedxmlはデフォルトで外部エンティティの解決や
        # 悪意あるDTDの読み込みを検知して例外を発生させる
        root = DefusedET.fromstring(xml_string)
        return root
        
    except DefusedET.EntitiesForbidden as e:
        # 外部エンティティの不正な利用を検知
        logging.warning(f"XXE攻撃の試行を検出しました: {e}")
        raise ValueError("セキュリティポリシー違反のXMLが含まれています。")
        
    except Exception as e:
        # その他のパースエラー
        logging.error(f"XMLパースエラー: {e}")
        raise ValueError("XMLの解析に失敗しました。")

# 使用例
if __name__ == "__main__":
    malicious_xml = '''<?xml version="1.0"?>
    <!DOCTYPE root [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
    <root>&xxe;</root>'''
    
    try:
        parse_secure_xml(malicious_xml)
    except ValueError as err:
        print(f"ブロック成功: {err}")

—

4. アプリケーション層だけじゃない!インフラ・WAFでの多層防御

開発者がどれだけ気をつけていても、古くなったサードパーティ製ライブラリのアップデート忘れや、思わぬ設定漏れが起きるのが現場というものだ。だからこそ、インフラ側でも二重・三重のガードを固める必要がある。

Nginx / リバースプロキシでのリクエスト検査

もしアプリケーションが直接生のXMLを受け取る必要がない、あるいはAPIゲートウェイの段階で特定のパターンを弾きたいのであれば、NginxなどのリバースプロキシやWAF(ModSecurity等)でペイロードを検査するのも有効な手段だ。

例えば、ModSecurityのルールセット(CRS)では、リクエストボディ内に <!ENTITY や SYSTEM といったキーワードが含まれている不正なXML構造を動的に検知して遮断する機能が標準で備わっている。

また、AWS環境であれば、IAMロールの設計において、不必要なインスタンスメタデータ(IMDSv2の強制)を設定し、万が一SSRFを伴うXXEを踏み抜いたとしても、最小限の権限しか渡さないという「ゼロトラスト」の思想を徹底してほしい。

—

シニアエンジニアからのメッセージ

セキュリティ対策に「銀の弾丸」は存在しない。だが、XXEに関しては「外部エンティティの読み込みをデフォルトでオフにする」、あるいは「安全なパーサーライブラリに置き換える」という明確な処方箋がある。

コードレビューの際、DOMDocument や SimpleXMLElement、あるいは標準のXMLパーサーが、何のオプションもなしに生の状態で見つかったら、それはもうイエローカードどころかレッドカードだ。チームメンバー全員がこのリスクを正しく恐れ、デフォルトでセキュアなコードを書く文化を定着させていこう。

手を動かす前に、まずは自分たちのシステムのパーサー設定を確認することだ。それが今日の最初のミッションだ。

コメント

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