こんにちは!新人IT担当者や、セキュリティの勉強を始めたばかりの皆さん、日々の開発やインフラ管理お疲れ様です。
「セキュリティ対策って、専門用語が多くてなんだか難しそう……」
そう感じていませんか?
今回は、Webアプリのちょっとした隙をつく「XXE(XML External Entity)攻撃」という恐ろしいサイバー攻撃について、身近な「家の防犯」に例えながら、分かりやすく紐解いていきたいと思います。
難しそうに見える攻撃の仕組みも、対策のコードも、一歩ずつ見ていけば必ず理解できます。それでは、一緒に安全なWebアプリの作り方を学んでいきましょう!
—
1. 身近な防犯で例える「XXE攻撃」の正体
まずは、攻撃のメカニズムをイメージしてみましょう。皆さんが暮らす家を想像してください。
玄関の鍵(パスワードや認証機能)をしっかり閉めていても、もし「郵便受け(データの入り口)」のフタがパカパカで、外から手を入れて中のものを自由に取り出せる状態だったらどうでしょう? 泥棒はわざわざ頑丈な玄関のドアを壊さなくても、郵便受けから手を突っ込んで、リビングのテーブルの上にある「重要書類(機密ファイル)」を引っ張り出してしまいますよね。
この「郵便受けの隙間」こそが、今回テーマにする XXE(XML External Entity)攻撃 の正体です。
XMLと「外部エンティティ」の便利な機能が裏目に
現代のWebシステムでは、システム同士がデータをやり取りするために、XMLというデータ形式がよく使われます。
このXMLには、「外部エンティティ」という非常に便利な機能があります。これは、ざっくり言うと「別の場所にあるファイルや、インターネット上の別のデータを読み込んで、XMLの一部として組み込む」という機能です。
開発者にとっては「プログラムを効率よく作るための便利な道具」なのですが、セキュリティの対策を忘れていると、これがそのまま「外部の攻撃者にサーバー内のファイルを勝手に読み取られてしまう道具」に化けてしまうのです。
—
2. 攻撃者はどうやってシステムをハックするのか?
では、攻撃者は具体的に何をするのでしょうか?
彼らは、ユーザーが入力するフォームやAPIのリクエスト(郵便受け)を通じて、悪意ある特製の「XMLデータ」をサーバーに送り込みます。
例えば、サーバー内にあるパスワードファイル(Linuxの /etc/passwd など)を読み取らせようとする攻撃コード(ペイロード)は、以下のような形をしています。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE root [
<!-- 外部エンティティを使ってサーバー内の秘密のファイルを指定する -->
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>
<!-- 読み込んだファイルを画面に表示させるように仕向ける -->
<name>&xxe;</name>
</root>
サーバー側のXMLパーサー(XMLデータを読み込んで理解するプログラム)が、このデータを何も疑わずに処理してしまうとどうなるでしょうか?
パーサーは「おっ、&xxe; って書かれているから、言われた通りに /etc/passwd の中身を取ってきて埋め込んであげよう!」と親切心(お人よし)を発揮してしまい、結果としてサーバーの機密情報が攻撃者の画面に丸見えになってしまうのです。怖いですよね。
—
3. 言語別・パーサー設定による防御の実装方法
「じゃあ、XMLなんて使わない方がいいの?」と思われるかもしれませんが、ご安心ください。XML自体が悪いわけではなく、「外部のファイルを読み込んじゃダメだよ」とパーサーにしっかりお説教(設定)をしておけば、この攻撃は完璧に防ぐことができます。
ここからは、実際の開発現場でよく使われる言語(PHP、Java、Python)を例に、安全なパーサーの設定方法を一緒に見ていきましょう!
PHP (DOMDocument / SimpleXML) の場合
PHPでは、外部エンティティの読み込みを無効化するために、libxml_disable_entity_loader という関数を使用します。XMLを処理するプログラムの一番最初にこれを書いておくのが鉄則です。
<?php
// 外部エンティティの読み込みを確実に禁止する(これ最重要です!)
libxml_disable_entity_loader(true);
// ユーザーからのXML入力を安全に読み込むための設定
$xmlInput = file_get_contents('php://input');
$dom = new DOMDocument();
// 外部エンティティの解決を無効化してロードする
$dom->loadXML($xmlInput, LIBXML_NOENT | LIBXML_DTDLOAD);
$data = $dom->saveXML();
echo "安全に処理されました: " . htmlspecialchars($data, ENT_QUOTES, 'UTF-8');
?>
Java (DocumentBuilderFactory) の場合
JavaでXMLを扱う場合、デフォルトの状態では外部エンティティの読み込みが有効になっていることが多いです。そのため、ファクトリーの設定を変更して、明示的に機能をオフにする必要があります。
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import java.io.StringReader;
public class SecureXmlParser {
public Document parseXml(String xmlData) throws Exception {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
// 【防御設定】DTD(外部参照の定義)を完全に無効化する
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
// あるいは、外部DTDや外部一般エンティティを個別に無効化する
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
DocumentBuilder db = dbf.newDocumentBuilder();
return db.parse(new InputSource(new StringReader(xmlData)));
}
}
Python (defusedxml) の場合
Pythonの標準ライブラリには、そのままではXXEに弱いパーサーも存在します。そのため、セキュリティコミュニティが推奨している安全なラッパーライブラリ defusedxml を使うのが最も確実でスマートな方法です。
# 事前にインストールが必要です: pip install defusedxml
from defusedxml import ElementTree as ET
def safe_parse_xml(xml_string):
try:
# defusedxml を使うことで、安全にパース(解析)が行われます
# 万が一、悪意あるXXEが含まれていても自動でブロックされます
root = ET.fromstring(xml_string)
print("XMLのパースに成功しました。安全なデータです。")
return root
except Exception as e:
print(f"不正なXMLデータ、または攻撃を検知しました: {e}")
return None
# テスト用のXML
malicious_xml = '<?xml version="1.0" encoding="UTF-8"?><root><name>Test</name></root>'
safe_parse_xml(malicious_xml)
—
4. まとめ:一歩ずつセキュアな開発者へ
今回は、XXE攻撃の仕組みと、言語ごとのパーサー設定による防御方法について解説しました。
- XXE攻撃とは?:XMLの「外部エンティティ機能」の隙をついて、サーバー内の機密ファイルを盗み出す攻撃。
- 対策のキモ:「外部ファイルを勝手に読み込ませない」ように、使用している言語のXMLパーサーでしっかりと制限(無効化)をかけること。
セキュリティ対策は、一度にすべてを完璧にやろうとすると息切れしてしまいます。ですが、「外部からの入力をそのまま信用しない」「デフォルトの設定をそのまま使わずに、安全な設定に書き換える」という意識を持つだけでも、皆さんが作るWebアプリの安全性は劇的に向上します。
一歩ずつ、確実にスキルアップしていきましょう!次の記事でも、実践的で役に立つセキュリティの知識をお届けしますので、ぜひ楽しみにしていてくださいね。
コメント