説明
エンティティを展開する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を使って本文のサイズを制限します。解析エラー時は処理を止め、エラー応答を一度だけ返します。