Ajv の allErrors:true によるリソース枯渇

Ajv の検証エラー全件収集によるリソース枯渇

説明

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 は接続のタイムアウトであり、同期的な検証の実行時間を制限するものではありません。

参考資料