XML外部エンティティの展開

XMLエンティティ展開によるファイル・ネットワークアクセスの危険

説明

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エンティティを重複してデコードしません。

参考資料