説明
libxmljsやlibxmljs2のnoent: trueは、エンティティ参照を実際の値へ展開します。信頼できないXMLが外部エンティティを宣言し、パーサーが読み込める場合は、ローカルファイルへのアクセスやサーバーサイドリクエストフォージェリ(SSRF)につながるおそれがあります。実際のファイル・ネットワークアクセスや展開の上限は、同梱されたlibxml2のバージョン、ビルド、ローダー設定によって異なります。
想定される影響
- 外部エンティティから
/etc/passwdなどを読み込まれ、内容が応答やログに漏えいする可能性があります。 - パーサーが
http://169.254.169.254などの内部・メタデータサービスへアクセスし、機密情報を取得されるおそれがあります。 - 大量または再帰的なエンティティ展開でCPUやメモリーを消費し、サービスが停止する可能性があります。
対処方法
- 信頼できないXMLでは
noentを有効にせず、falseを維持してください。 - DTDが不要なら拒否してください。外部DTDやエンティティを読み込む追加オプションや独自のローダーも確認してください。
&や<などの定義済み参照はXMLパーサーが処理します。解析済みのテキストをHTMLエンティティデコーダーで再度変換しないでください。- 入力サイズと形式を制限し、解析エラーを処理してください。展開量、メモリー、実行時間の制限も確認してください。
- ネットワークとファイルの権限を最小限にしてください。
noent: falseだけですべての外部リソースアクセスを防げるわけではありません。
例
変更前
javascript
const express = require('express');
const libxml = require('libxmljs');
const app = express();
app.use(express.text({ type: '*/*' }));
// 外部からのXMLをnoent: trueで解析
app.post('/api/parse-xml', (req, res) => {
const xmlPayload = String(req.body || '');
// noent: trueでは外部エンティティの展開でファイルアクセス・SSRF・DoSのおそれ
const doc = libxml.parseXmlString(xmlPayload, { noent: true });
res.json({ root: doc.root().name() });
});
app.listen(3000);
変更後
javascript
const express = require('express');
const libxml = require('libxmljs');
const app = express();
app.use(express.text({ type: '*/*' }));
// noentを無効にし、DTDを拒否
app.post('/api/parse-xml', (req, res) => {
const xmlPayload = String(req.body || '');
// 外部エンティティを宣言できるDTDを拒否
if (/<!DOCTYPE/i.test(xmlPayload)) {
return res.status(400).send('DTD is not allowed');
}
// noentを明示的に無効化
const doc = libxml.parseXmlString(xmlPayload, { noent: false });
// パーサーが返したテキストを再度デコードしない
const textContent = doc.root().text();
res.json({ root: doc.root().name(), text: textContent.slice(0, 200) });
});
app.listen(3000);
変更前はエンティティの展開を有効にします。変更後はExpressが文字列にデコードした入力からDTD宣言を拒否し、noentを無効にします。この単純な検査ですべてのXML入力経路を保護できるとは考えないでください。パーサーの外部読み込み設定を確認し、不正なXMLへのエラー応答を追加してください。例では解析済みのテキストをそのまま使い、HTMLエンティティを重複してデコードしません。