XXEで内部ファイルを読み取る|XML外部エンティティの悪用
第7話ではOAuthのredirect_uri検証の緩さを突きました。第8話は、もっと素朴に見える機能——「XMLを受け取って処理する」だけの入力欄が、サーバーの内部ファイルを丸ごと読み出す入口に変わってしまう様子を体験します。鍵になるのは、XMLが標準で備える「エンティティ」という古くからの仕組みです。
📋 目次
🗂️「XMLを受け取るだけ」の機能が危険になる理由
データ形式のはずのXMLが、命令を含んで届く
「設定ファイルをXMLでインポート」という、ありふれた機能
多くのアプリには「設定をXMLでまとめてインポートする」「他社サービスからXML形式でデータを取り込む」といった機能があります。一見、ただ受け取ったXMLを読み込むだけの無害な処理です。ところがXMLには、文書の中に「このキーワードはこの内容に置き換えてね」という指示(エンティティ)を埋め込める仕組みがあり、その置き換え先として「サーバー上のファイル」を指定できてしまう場合があります。これがXXE(XML External Entity、XML外部エンティティ)です。
普通、外部から受け取るデータは「ただのデータ」であって、サーバーに何かを命令する力は持たないはずです。しかしXMLのエンティティ機能は、データの中に「この外部リソースを読み込んで埋め込め」という小さな命令を紛れ込ませることを許します。攻撃者はこの仕組みを使い、本来は外部に見せないはずのサーバー内部のファイル(設定ファイル、認証情報、ソースコードなど)を、XMLの応答の中に埋め込ませて読み出します。データのつもりで受け取ったものが、実は命令を含んでいた——これが信頼境界の崩れ方です。
この話の前提知識
上級編第2話のSSRFと同じく、今回のテーマも「サーバーが、外部から渡された入力をどこまで信じてよいか」という信頼境界の問題です。SSRFが「URLを信じすぎる」話だったのに対し、XXEは「XMLの中の指示を信じすぎる」話です。
- 上級編 第2話「SSRFでクラウド内部に侵入|メタデータエンドポイントの罠」(信頼境界の考え方)
XXEは新しい攻撃ではなく、XMLが広く使われ始めた頃から知られている古典的な脆弱性クラスです。それでも今なお、XMLを扱うあらゆる場面(設定ファイル、ドキュメント形式、SOAP API、SAML認証など)で繰り返し報告され続けています。次のセクションでは、その鍵となる「エンティティ」の仕組みを見ていきます。
🧩 XMLエンティティの仕組み|内部・外部・パラメータ
「キーワードを中身に置き換える」機能の3つの種類
XMLのエンティティは、文書の冒頭にある<!DOCTYPE>(DTD=文書型定義)の中で宣言します。一番身近なのは「内部エンティティ」で、これは単なる文字列の置き換えです。たとえば<!ENTITY item "ノート">と宣言しておくと、本文中の&item;がすべて「ノート」に置き換わります。これだけなら便利な略語機能にすぎません。問題は次の「外部エンティティ」です。
| 種類 | 書き方の例 | 何が起きるか |
|---|---|---|
| 内部エンティティ | <!ENTITY x "値"> | 本文の&x;が「値」に置き換わる(ただの略語) |
| 外部エンティティ(SYSTEM) | <!ENTITY x SYSTEM "file:///path"> | 本文の&x;が、指定したファイルの中身に置き換わる(危険) |
| 外部エンティティ(URL) | <!ENTITY x SYSTEM "http://..."> | 指定したURLへの通信が発生する(SSRFにつながる) |
| パラメータエンティティ | <!ENTITY % p ...> | DTD内部で使う特殊なエンティティ。高度な攻撃で悪用される |
SYSTEM + file:// が、ファイル読み取りの正体
SYSTEM "file:///etc/passwd"のように書くと、パーサーは「このエンティティの中身は、このファイルを読んだ結果だ」と解釈します。そして本文中で&x;を参照すると、そのファイルの中身がXMLの一部として埋め込まれ、応答に乗って返ってきます。攻撃者はこれを使って、設定ファイルや認証情報ファイルなど、本来は外から見えないファイルの中身を読み出します。同じ仕組みでhttp://を指定すれば、第2話で学んだSSRFにもつながります。XXEは「ファイル読み取り」と「SSRF」の両方の入口になりうる、応用範囲の広い脆弱性です。
🔍 なぜ見つけにくいのか|パーサーが標準で解決してしまう
「正しく動くXMLパーサー」ほど、外部エンティティを解決する
XXEの厄介なところは、攻撃が「XMLの仕様どおりの正しい動作」として起きる点です。古くからあるXMLパーサーの多くは、外部エンティティの解決を標準で有効にしていました。つまり開発者が何も特別なことをしなくても、パーサーが「親切に」ファイルを読みに行ってしまうのです。アプリ側のコードには「ファイルを読む」という処理が一行も書かれていないのに、XMLライブラリの内部でファイルが読まれてしまうため、コードレビューでも見落とされがちです。
「自分のコードには file を読む処理なんて無い」という思い込み
開発者は「うちのコードはXMLをパースしてフィールドを取り出しているだけで、ファイルアクセスなんてしていない」と考えがちです。しかし実際にファイルを読むのは、開発者が書いたコードではなく、その下で動くXMLライブラリです。だからこそ、XXE対策は「自分のコードを直す」のではなく「XMLパーサーの設定で外部エンティティを無効にする」という、ライブラリの設定の問題になります。
次の実践チャレンジでは、この「親切すぎるパーサー」を安全に再現した環境で、XMLインポート機能を通じてサーバー内部のファイルを読み出す流れを体験します。なお、このシミュレーターは実際のファイルには一切アクセスせず、あらかじめ用意した架空のファイル一覧の中だけで動作します。
💻 実践チャレンジ:ToDoループのXMLインポートからファイルを読む
外部エンティティを定義して、内部ファイルの中身を引き出す
架空のToDoアプリ「ToDoループ」には、設定をXMLでインポートする機能があります。下のインポート欄に入力したXMLは、外部エンティティの解決が有効な(=脆弱な)パーサーで処理されます。最初は内部エンティティの例が入っています。これを外部エンティティ(SYSTEM "file://...")に書き換えて、サーバー内部のファイルを読み出してください。このサーバーには、次のファイルが存在することがわかっています。
$ ToDoループ – 設定XMLインポート(脆弱なパーサー)
パース結果(エンティティ解決後):
ステージ1をクリアすると、ステージ2が解放されます。両方クリアした時点でスコアが確定します。
📊 ステージ進捗: 0/2|挑戦回数: 0回
上のインポート欄のXMLを書き換えて、file:///etc/app/build.txtの中身を読み出してください。パース結果の中に合言葉(passphrase=の値)が見つかるので、それを入力してください(例: XXE-ENTITY-OK)。
内部エンティティ<!ENTITY item "ノート">を、外部エンティティ<!ENTITY item SYSTEM "file:///etc/app/build.txt">に書き換えてみましょう。本文の&item;はそのままで構いません。
次のXMLを貼り付けてパースしてください:<?xml version="1.0"?><!DOCTYPE c [<!ENTITY x SYSTEM "file:///etc/app/build.txt">]><c>&x;</c>
結果の中のpassphrase=の右側が合言葉です。
ステージ1で読んだbuild.txtの中に、もう1つのファイルの場所(secret_config=)が書かれていたはずです。今度はそのファイルを外部エンティティで読み出してください。中にFLAG=の形でフラグが入っています。表示されたフラグをそのまま入力してください。
build.txtの中のsecret_config=/etc/app/secret/flag.confが、次に読むべきファイルです。エンティティのSYSTEM "file://..."の部分をfile:///etc/app/secret/flag.confに変えてパースしてください。
🛡️ 防御側の視点|XXEをどう止めるか
「自分のコード」ではなく「パーサーの設定」で止める
- 外部エンティティ・DTDを無効にする:XMLパーサーの設定で、外部エンティティの解決とDTDの処理そのものを禁止する。これがXXE対策の本丸で、多くのライブラリには専用の設定がある
- そもそもDTDを許可しない:信頼できない入力に対しては、DTD(
<!DOCTYPE>)を含むXMLを丸ごと拒否するのが最も確実 - 可能ならXMLをやめてJSONを使う:JSONにはエンティティのような外部リソース読み込みの仕組みがないため、XXEの心配がない。新規設計では検討する価値がある
- ライブラリを安全な既定値のものに保つ:近年のXMLライブラリは外部エンティティを既定で無効にしているものが増えている。古いライブラリやレガシー設定に注意する
- WAFは補助に留める:
<!ENTITYなどのパターンを検知するWAFは入口を狭めるが、エンコーディングや変種で回避されうるため、根本対策はあくまでパーサー設定
「データを受け取る機能」に「ファイルを読む力」を与えない
XXEの本質は、データ形式であるはずのXMLに、外部リソースを読み込む力が標準で備わっていることにあります。だからこそ防御も「その力を最初から無効にしておく」のが正解です。入力をどう検証するかではなく、パーサーにそもそも危険な能力を持たせないという発想が重要です。
外部から受け取ったものを、どこまでの権限で処理するか——XXEもまた、第2話のSSRFや第7話のOAuthと同じ「信頼境界」の物語です。次のセクションで今回の学びをまとめます。
📝 まとめ+FAQ+次回予告
データのつもりが、命令だった
第8話では、XMLの外部エンティティ機能を悪用して、サーバー内部のファイルを読み出すXXEを体験しました。CTFの「フラグ探し」ではなく、XMLを扱うあらゆる場面で今なお報告され続けている、息の長い脆弱性クラスそのものです。
・XMLのエンティティは「キーワードを中身に置き換える」仕組みで、置き換え先にファイルを指定できる
・<!ENTITY x SYSTEM "file://...">で内部ファイルの中身が応答に埋め込まれて読み出される
・攻撃はXMLパーサーの「正しい動作」として起き、アプリのコードには現れないため見つけにくい
・防御の本丸は、パーサーの設定で外部エンティティ・DTDを無効にすること
Q. 実際のサービスでもXXEは今でも見つかりますか?
はい。XMLを扱う設定インポート、ドキュメント形式(オフィス文書の内部はXMLです)、SOAP API、SAML認証など、XMLが使われる場面でXXEは今なお報告され続けています。古典的でありながら、いまだに現役の脆弱性クラスです。
Q. なぜ実在のファイルパスやサービス名を使わないのですか?
実在のサービスや実際のファイルシステムを対象にすると、本物の攻撃手順の解説と誤認されるおそれがあるためです。本話では概念と構造を正確に保ったまま、架空のアプリ「ToDoループ」と、あらかじめ用意した架空のファイル一覧の中だけで動作させています。
Q. このシミュレーターは本当に私のPCのファイルを読んでいませんか?
読んでいません。ファイルの解決は、ページ内にあらかじめ用意した「パス→架空の中身」の対応表を文字列として参照しているだけで、実際のfile://スキームやファイルシステムには一切アクセスしていません。一覧に無いパスを指定してもエラーになるだけです。
Q. 進捗やスコアの情報はどこかに送信されますか?
送信されません。すべてブラウザのlocalStorage(あなたの端末内)だけに保存され、外部のサーバーには一切送信されません。入力したXMLもどこにも送られず、ページ内で処理されるだけです。
複合チャレンジ|SSRF×OAuth×XXEを繋ぐ侵入経路
第2話・第7話・第8話で学んだ3つの技術を、1つの侵入シナリオに統合します。単発の脆弱性が連鎖したとき、攻撃がどこまで届くのかを体験します。
📚 参考情報
- OWASP Top 10(A05:2021 Security Misconfiguration / 旧A4:2017 XML External Entities)
- CWE-611(Improper Restriction of XML External Entity Reference)
- OWASP XML External Entity Prevention Cheat Sheet


コメント