説明
Ajv の allErrors:true は、検証で見つかったエラーをすべて収集します。大量のエラーオブジェクトや文字列の生成と、スキーマの制約の評価を続ける処理により、CPU とメモリの使用量が増える場合があります。攻撃者が多くのルールに違反する入力や巨大で深くネストした JSON を送り、リソースを消費させてサービス拒否(DoS)を引き起こすおそれがあります。
想定される影響
- 過剰なエラー収集や計算により、応答の遅延やプロセスの停止が起こる可能性があります。
- 大量のエラーオブジェクトやメッセージがヒープメモリを消費し、メモリ不足や再起動につながる場合があります。
- 検証の継続によって処理時間が延び、サービス全体の処理能力が低下するおそれがあります。
- 大量のエラーログが監視コストを増やし、必要な情報を見つけにくくする場合があります。
対処方法
- 本番環境では
allErrors:trueを使わず、後続のエラー収集を止める既定のfalseを維持してください。 - 開発環境でデバッグが必要な場合だけ、
DEBUG_VALIDATE=1などの環境変数で有効にしてください。 - リクエスト本文のサイズと接続時間を制限してください。リクエストのタイムアウトだけでは、同期的な検証の CPU 処理を中断できません。
- レート制限やサーキットブレーカーで過剰なトラフィックを抑えてください。
- 信頼できるスキーマだけを使用し、
oneOfやanyOfなどの制約を過剰に組み合わせないでください。 maxItemsやmaxPropertiesなどで入力構造の上限を定めてください。
例
変更前
javascript
import express from "express";
import Ajv from "ajv";
const app = express();
app.use(express.json({ limit: "1mb" }));
// BAD: 本番環境で allErrors:true を使用(すべてのエラーを収集)
const ajv = new Ajv({ allErrors: true });
const userSchema = {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string" },
tags: { type: "array", items: { type: "string" } },
},
required: ["name", "tags"],
};
const validateUser = ajv.compile(userSchema);
app.post("/users", (req, res) => {
const valid = validateUser(req.body);
if (!valid) {
// 大量のエラーを生成して返すと CPU とメモリを消費
return res.status(400).json({ errors: validateUser.errors });
}
res.send("ok");
});
app.listen(3000);
変更後
javascript
import express from "express";
import Ajv from "ajv";
const app = express();
// リクエストサイズとタイムアウトを設定
app.use(express.json({ limit: "512kb" }));
app.use((req, res, next) => {
req.setTimeout(5000);
next();
});
// GOOD: 既定の fail-fast 動作。allErrors は開発時だけ有効化
const enableAllErrors = process.env.NODE_ENV === "development" && process.env.DEBUG_VALIDATE === "1";
const ajv = new Ajv({ allErrors: enableAllErrors });
const userSchema = {
type: "object",
additionalProperties: false,
properties: {
name: { type: "string" },
tags: {
type: "array",
maxItems: 50,
items: { type: "string", maxLength: 64 },
},
},
required: ["name", "tags"],
};
const validateUser = ajv.compile(userSchema);
app.post("/users", (req, res) => {
const valid = validateUser(req.body);
if (!valid) {
// 大量のエラーオブジェクトを公開せず、要約したメッセージを返す
return res.status(400).json({ message: "Invalid payload" });
}
res.send("ok");
});
app.listen(3000);
説明:
- 変更前:
allErrors:trueは検証に失敗しても制約の確認とエラーの収集を続けます。多数の制約に違反する入力により、エラー生成や計算がリソースを消費し、DoS につながる可能性があります。 - 変更後: 開発環境でデバッグフラグを設定した場合だけ
allErrorsを有効にします。それ以外は、追加のエラー収集を減らす既定の動作を使い、本文サイズとmaxItems・maxLengthで処理量を制限します。req.setTimeoutは接続のタイムアウトであり、同期的な検証の実行時間を制限するものではありません。