XML内部エンティティの展開によるサービス拒否

XMLエンティティの展開(Billion Laughs)

説明

エンティティを展開するXMLパーサーでは、DTD内で深く入れ子にしたエンティティにより、小さな文書から巨大な文字列が生成され、メモリやCPUを消費し尽くすことがあります。node-expatなどを使う場合は、利用するバージョンの展開動作とリソース制限を確認してください。展開を十分に制限しないと、Billion Laughs型のサービス拒否につながるおそれがあります。

想定される影響

  • プロセスの応答不能やサービス停止
  • メモリ不足による終了
  • エンティティの展開や解析によるCPUの過剰使用
  • クラッシュ、再起動ループ、可用性障害の波及
  • 不要な自動スケーリングやリソース費用の増加

対処方法

  • 可能ならDTDやエンティティの展開を無効にし、DOCTYPE宣言を拒否してください。
  • 展開を適切に制御できるパーサーか、saxのようにDTDの独自エンティティを展開しないストリーミングパーサーを使ってください。
  • リクエストのサイズ、解析時間、ノード数、深さ、テキスト長を制限してください。
  • 許可するスキーマで検証し、<!DOCTYPE などのDTD宣言を拒否してください。
  • プロキシやWebサーバーでリクエストサイズを制限し、アプリケーションの同時実行数やキューを管理してください。

例

変更前

javascript
const express = require("express");
const expat = require("node-expat");
const app = express();
app.use(express.text({ type: "application/xml" }));

app.post("/parse", (req, res) => {
  const xml = req.body; // 信頼できない入力
  const p = new expat.Parser();
  p.on("startElement", () => {});
  p.on("text", () => {});
  // 展開を十分に制限しないとXML爆弾につながるおそれがあります。
  p.write(xml);
  res.send("parsed");
});

module.exports = app;

変更後

javascript
const express = require("express");
const sax = require("sax");
const app = express();
// 本文のサイズを制限します。
app.use(express.text({ type: "application/xml", limit: "200kb" }));

app.post("/safe-parse", (req, res) => {
  const xml = req.body || "";

  // DOCTYPE/ENTITYなどのDTD宣言を拒否します。
  const suspicious = /<!DOCTYPE|<!ENTITY/i.test(xml);
  if (suspicious) {
    return res.status(400).send("DTD/ENTITY not allowed");
  }

  // saxはDTDの独自エンティティを展開しません。
  const parser = sax.parser(true); // strict mode
  parser.onopentag = () => {};
  parser.ontext = () => {};
  parser.onerror = (e) => {
    throw e;
  };
  try {
    parser.write(xml).close();
  } catch {
    return res.status(400).send("invalid xml");
  }
  return res.send("ok");
});

module.exports = app;

説明:

  • 変更前: 信頼できないXMLをエンティティを展開するパーサーに渡します。バージョンやリソース制限によって展開を十分に抑えられない場合、小さな入力でもメモリやCPUを枯渇させるおそれがあります。
  • 変更後: DTD/ENTITY宣言を拒否し、独自エンティティを展開しないsaxを使って本文のサイズを制限します。解析エラー時は処理を止め、エラー応答を一度だけ返します。

参考資料