【実務・中級編】 XML外部エンティティ (XXE) 攻撃の仕組みとパーサー設定 – 暗号理論・認証基盤 & エンドポイントセキュリティ防御ガイド

おい、そこの君。ちょっと手を止めて画面を見てくれ。

今日も今日とて、あちこちのシステムから「API連携のレスポンスがおかしい」「設定ファイルの一部が外部に吸い出されている気がする」なんて悲鳴のような相談が舞い込んでいる。原因を辿っていくと、大抵はレガシーなデータフォーマットを甘く見た設計、そしてXML外部エンティティ(XXE)脆弱性に行き着く。

暗号理論や認証基盤の強固さにいくら酔いしれていても、その玄関口であるパーサーの設定がガバガバであれば、難攻不落の城壁に「勝手に開いてください」とご丁寧に裏口の鍵をぶら下げているようなものだ。

今回は、攻撃者がどのようにXMLパーサーの盲点を突き、サーバー内の機密ファイルを強奪するのか、その泥臭い手口を明かした上で、現場で即座に使える決定版の防御策を叩き込んでやろう。心して聞くように。

—

1. なぜ今、XXEなのか? 攻撃者が狙うパーサーの盲点

WebのトレンドはすっかりJSON全盛だが、企業間連携(B2B)、古いSOAP API、SVG画像のアップロード処理、そして総務省や金融機関系のシステムでは、今でも当たり前のようにXMLが使われている。

XMLには、文書内で使い回す定数を定義するための「エンティティ」という強力な機能が存在する。そして、そのエンティティの参照先として、ローカルファイルパスや外部URLを指定できてしまう規格上の仕様(DTD:文書型定義)が組み込まれている。

攻撃者はこの仕様を悪用する。
「ねえ、XMLパーサー君、このデータ処理するときについでに /etc/passwd の中身を読み込んで、変数に代入して出力画面に混ぜ込んでくれない?」とリクエストに細工するわけだ。パーサーが親切心からそれを実行してしまうことで、SSRF(サーバー側リクエスト偽造)や機密情報の露呈、最悪の場合はリモートコード実行(RCE)の踏み台にされてしまう。

—

2. 恐怖のPoC:ローカルファイル強奪のメカニズム

百聞は一見に如かずだ。脆弱な設定のまま放置されたXML受け入れエンドポイントに対して、攻撃者が送りつけるリクエストのリアルな構造を見てみよう。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
  <!-- 外部DTDを通じてローカルファイルをエンティティとして定義する -->
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
  <name>&xxe;</name>
  <message>システムの死活監視データです</message>
</root>

もし、このXMLを処理するバックエンドのプログラムが、DTDの解釈や外部リソースの読み込みを制限していなかったらどうなるか。
サーバーは平然と自身のOS内の /etc/passwd を読み込み、&xxe; の部分を展開してレスポンスに含めて返してしまう。

「うちはクラウドだから /etc/passwd なんて古いファイルはないよ」と高を括っているなら大間違いだ。Windows環境であれば file:///c:/windows/win.ini になるし、内製システムのソースコードやデータベースの接続文字列が書かれた設定ファイル、あるいはクラウドのメタデータエンドポイント(http://169.254.169.254/latest/meta-data/)を叩かれて、IAMロールのクレデンシャルをごっそり抜き取られるのがオチだ。

—

3. 完全防御の鉄則:XMLパーサーのセキュア設定

XXEを防ぐためのアプローチはシンプルかつ明確だ。
「外部DTDの読み込みを完全に禁止する(Disable DTDs / External Entities)」 これに尽きる。

フレームワークや言語のデフォルト設定では、利便性を優先してこれらのセキュリティ機能がオフ(=脆弱な状態)になっていることが多々ある。ここからは、現場のエンジニアがコピペして即座に安全性を担保できる、主要言語別のセキュアな実装コードを提示する。

【PHP / Libxml】の場合

PHPでXMLを扱う際、よく使われる simplexml_load_string や DOMDocument は、デフォルトで外部エンティティをロードしてしまう。処理の直前で明示的に無効化する関数を挟む必要がある。

<?php
/**
 * セキュアなXMLパース処理(PHPサンプル)
 * 外部エンティティの読み込みを完全に遮断し、XXEを防ぐ
 */
function secure_xml_parse(string $xmlData) {
    // 外部エンティティのロードを確実に禁止する
    libxml_disable_entity_loader(true);
    
    // LIBXML_NOENT はエンティティ置換を有効にするフラグなので絶対に使わないこと!
    // LIBXML_DTDLOAD や LIBXML_DTDATTR も無効であることを確認する
    $dom = new DOMDocument();
    
    // Libxmlの内部オプションで外部DTDの読み込みを明示的にブロック
    $internalErrors = libxml_use_internal_errors(true);
    
    // 安全なフラグのみを指定してロード(必要に応じて LIBXML_NONET 等を併用)
    $loadSuccess = $dom->loadXML($xmlData, LIBXML_NONET);
    
    if (!$loadSuccess) {
        // パースエラーのハンドリング
        $errors = libxml_get_errors();
        libxml_clear_errors();
        libxml_use_internal_errors($internalErrors);
        throw new \InvalidArgumentException("不正なXMLフォーマットです。");
    }

    libxml_use_internal_errors($internalErrors);
    return $dom;
}

// 実行例
$userXml = '...'; // ユーザーからの入力
try {
    $xmlObj = secure_xml_parse($userXml);
    // 安全にデータを処理...
} catch (\Exception $e) {
    // ログに記録して処理中断
    error_log($e->getMessage());
}

【Python / Defusedxml】の場合

Pythonの標準ライブラリ(xml.etree.ElementTree や xml.dom.minidom 等)は、XXEに対して脆弱な設計になっていることが多い。そのため、セキュリティ界隈では定番のサードパーティライブラリ defusedxml を使うのがベストプラクティスだ。

"""
セキュアなXMLパース処理(Pythonサンプル)
標準のxmlモジュールの代わりに `defusedxml` を使用し、
XXE、爆弾XML(Billion Laughs攻撃)などをまとめて無効化する
"""
import defusedxml.ElementTree as ET

def parse_secure_xml(xml_string: str):
    try:
        # defusedxmlは悪意あるDTDや外部実体参照が含まれている場合、即座に例外をスローする
        root = ET.fromstring(xml_string)
        return root
    except defusedxml.common.DefusedXmlException as e:
        # 攻撃の兆候としてアラートを飛ばすべきインシデント
        print(f"[SECURITY ALERT] 潜在的なXXE攻撃または不正なXMLを検知しました: {e}")
        raise ValueError("安全ではないXML構造が検出されました。")
    except Exception as e:
        print(f"XMLパースエラー: {e}")
        raise

# 実行例
# pip install defusedxml で事前にインストールしておくこと
# xml_data = "..."
# root_node = parse_secure_xml(xml_data)

【Node.js / sax や xml2js】の場合

JavaScript(Node.js)でXMLを扱う場合も、パーサーライブラリのオプション設定に細心の注意が必要だ。例えば広く使われる libxmljs を使う場合、デフォルトで外部エンティティの読み込みが無効化されているケースが多いが、古いバージョンや他のパーサー(例: node-xml など)では明示的な設定が必要になる。

/**
 * セキュアなXMLパース処理(Node.js / libxmljs サンプル)
 */
const libxmljs = require("libxmljs");

function parseXmlSafely(xmlString) {
    try {
        // libxmljsの標準的なパース処理
        // オプションでnoent(no entity substitution)やnocdataなどを適切に管理
        // 基本的に最新の安全なラッパー関数を使用し、非推奨な外部参照オプションは有効化しない
        const xmlDoc = libxmljs.parseXml(xmlString, {
            noent: false,     // 外部エンティティを展開しない(重要)
            nonet: true,      // ネットワーク接続を伴うエンティティの取得を禁止
            recover: false    // 不正なXMLはエラーとして弾く
        });
        
        return xmlDoc;
    } catch (err) {
        console.error("XMLの解析に失敗しました。セキュリティ例外の可能性があります。", err.message);
        throw new Error("Invalid XML input");
    }
}

module.exports = { parseXmlSafely };

—

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

セキュリティの基本は「多層防御」だ。開発チームがどれだけ頑張ってコードを書いても、サードパーティ製のミドルウェアに脆弱性が眠っていたり、過去の遺物のような古いコードがどこかに残っていたりする。

そのため、ネットワーク境界やリバースプロキシ層でもXXEの兆候をブロックする構えを作っておくべきだ。

Nginxでのリクエスト制限と検査

Nginx自体はXMLパーサーを持たないが、不正な文字列(<!ENTITY や SYSTEM などのDTD定義キーワード)がリクエストボディに含まれていないかを ngx_http_lua_module などで簡易検査するか、WAF(Web Application Firewall)を前段に噛ませるのが定石だ。

AWS環境であれば、AWS WAFのマネージドルールグループにある Core rule set (CRS) の中で、XXEやXMLインジェクションに関するシグネチャを有効化しておくだけで、多くの攻撃リクエストをエッジで自動遮断できる。

カスタムルールを組む場合は、リクエストボディに <!ENTITY や file://、http:// が含まれる怪しいXML構文を検知して 403 Forbidden を返すよう、正規表現による検査を導入すると非常に効果的だ。

—

5. シーフエンジニアからの総括

XXEは、発生メカニズム自体は非常に古典的でありながら、ひとたび踏み抜けば内部ネットワークの全容把握や重要情報の流出に直結する、極めて危険度の高い脆弱性だ。

「うちのアプリは内部の人間しか使わないから大丈夫」
「モダンなフレームワークを使っているから自動で守られているはず」

そんな甘い考えは今日この瞬間にゴミ箱へ捨ててくれ。セキュリティは「推測」ではなく「検証と確証」で成り立つものだ。今すぐ自社のコードベースを漁り、DOMDocument や各種XMLパーサーの初期化処理に LIBXML_NONET が入っているか、外部エンティティが無効化されているかを確認し、テスト環境で今回のPoCを模したリクエストを流して安全性を自分の目で確かめてほしい。

自分のシステムは、自分で守る。プロのエンジニアなら、その気概を忘れないでくれ。頼んだぞ。

コメント

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