DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude CodeでAPI不正利用検知を設計する:異常検知・ボット対策・自動ブロック

APIの不正利用は、サービスの安定性・コスト・セキュリティを直撃する。Claude Codeを使ってリスクスコアリングから自動ブロックまでを設計する実践ガイド。

リスクスコアリング:0〜100の数値で脅威を定量化

API不正利用検知の核心は「リスクスコア」。0〜100のスコアを算出し、閾値に応じてアクションを変える。

スコア 0〜29  : 正常
スコア 30〜69 : 要注意 → CAPTCHAチャレンジ
スコア 70〜100: 高リスク → 自動ブロック
Enter fullscreen mode Exit fullscreen mode

Claude Codeへのプロンプト例:

以下の5パターンをもとにリスクスコア計算関数を実装してください。
各パターンの重みを設定し、合計が100を超えないようにしてください。
Enter fullscreen mode Exit fullscreen mode

5つの検知パターン

1. 認証失敗率(Fail Rate)

// 直近10分の認証失敗率
const failRate = failCount / totalRequests;
const score1 = Math.min(failRate * 100, 30); // 最大30点
Enter fullscreen mode Exit fullscreen mode

失敗率30%超えで最大スコアに達する。ブルートフォース攻撃の典型パターン。

2. リクエストレート(Request Rate)

// 1分あたりのリクエスト数
const rpm = requestCount / windowMinutes;
// 正常ユーザーは通常60rpm以下
const score2 = Math.min(Math.max(0, rpm - 60) / 40 * 25, 25); // 最大25点
Enter fullscreen mode Exit fullscreen mode

100rpmを超えたあたりから自動化を疑う閾値設定が有効。

3. エンドポイント多様性(Endpoint Diversity)

// 短時間で多数の異なるエンドポイントを叩いているか
const uniqueEndpoints = new Set(recentRequests.map(r => r.path)).size;
const score3 = Math.min(uniqueEndpoints / 20 * 20, 20); // 最大20点
Enter fullscreen mode Exit fullscreen mode

スキャナーは網羅的にエンドポイントを探索する。多様性が高いほど怪しい。

4. アカウントのマルチIP(Multi-IP per Account)

// 同一アカウントが複数IPから操作
const uniqueIPs = await getUniqueIPsForAccount(accountId, '1h');
const score4 = Math.min((uniqueIPs - 1) * 5, 15); // 最大15点
Enter fullscreen mode Exit fullscreen mode

1アカウントが短時間で3IP以上から接続 → クレデンシャルスタッフィングの疑い。

5. ヘッドレスUA(Headless UA Detection)

// HeadlessChrome / Puppeteer / Selenium のシグネチャ検出
const headlessPatterns = [
  /HeadlessChrome/i,
  /PhantomJS/i,
  /Selenium/i,
  /webdriver/i
];
const isHeadless = headlessPatterns.some(p => p.test(userAgent));
const score5 = isHeadless ? 10 : 0; // 最大10点
Enter fullscreen mode Exit fullscreen mode

ボットタイミング検出:変動係数(CV)の活用

人間の操作には自然なばらつきがある。ボットのリクエスト間隔は機械的に均一になりがち。

function calculateCV(intervals) {
  const mean = intervals.reduce((a, b) => a + b) / intervals.length;
  const variance = intervals.reduce((sum, x) => sum + Math.pow(x - mean, 2), 0) / intervals.length;
  const stdDev = Math.sqrt(variance);
  return stdDev / mean; // 変動係数
}

// CV < 0.1 は機械的すぎる(人間なら通常0.3以上)
const cv = calculateCV(requestIntervals);
if (cv < 0.1 && intervals.length >= 10) {
  riskScore += 15; // ボットタイミングペナルティ
}
Enter fullscreen mode Exit fullscreen mode

CVが低いほど均一 = ボットの可能性が高い。Claude Codeでこのロジックを実装する際、「正規分布のシミュレーションでCVの閾値を検証して」と指示すると適切な閾値が導き出せる。

RedisパイプラインでのAPI記録

リアルタイム検知にはRedisのパイプラインが必須。1リクエストあたりのレイテンシを最小化しながら複数のカウンターを更新する。

async function recordApiRequest(ctx) {
  const { ip, accountId, path, statusCode, userAgent, responseTime } = ctx;
  const now = Date.now();
  const windowKey = Math.floor(now / 60000); // 1分ウィンドウ

  const pipeline = redis.pipeline();

  // IPベースのカウンター(TTL: 10分)
  pipeline.hincrby(`ip:${ip}:${windowKey}`, 'total', 1);
  pipeline.expire(`ip:${ip}:${windowKey}`, 600);

  if (statusCode >= 400) {
    pipeline.hincrby(`ip:${ip}:${windowKey}`, 'fail', 1);
  }

  // エンドポイント記録
  pipeline.sadd(`ip:${ip}:endpoints:${windowKey}`, path);
  pipeline.expire(`ip:${ip}:endpoints:${windowKey}`, 600);

  // アカウントIPセット
  if (accountId) {
    pipeline.sadd(`account:${accountId}:ips`, ip);
    pipeline.expire(`account:${accountId}:ips`, 3600);
  }

  // リクエスト間隔記録(タイミング分析用)
  pipeline.lpush(`ip:${ip}:intervals`, now);
  pipeline.ltrim(`ip:${ip}:intervals`, 0, 99); // 最新100件
  pipeline.expire(`ip:${ip}:intervals`, 600);

  await pipeline.exec();
}
Enter fullscreen mode Exit fullscreen mode

パイプラインにより、1回のネットワーク往復で全カウンターを更新できる。

自動ブロックの実装

async function enforceRiskScore(ip, score) {
  if (score >= 70) {
    // 自動ブロック:15分間
    await redis.setex(`blocked:${ip}`, 900, score.toString());
    await notifySlack(`🚨 IP ${ip} blocked (score: ${score})`);
    return { action: 'block', retryAfter: 900 };
  }

  if (score >= 30) {
    // CAPTCHAチャレンジ
    return { action: 'captcha', score };
  }

  return { action: 'allow', score };
}

// Expressミドルウェアとして組み込む
app.use(async (req, res, next) => {
  const ip = req.ip;

  // ブロックリストチェック
  const blocked = await redis.get(`blocked:${ip}`);
  if (blocked) {
    return res.status(429).json({
      error: 'Too Many Requests',
      retryAfter: await redis.ttl(`blocked:${ip}`)
    });
  }

  const score = await calculateRiskScore(ip, req);
  const enforcement = await enforceRiskScore(ip, score);

  if (enforcement.action === 'block') {
    return res.status(429).json({ error: 'Blocked', score });
  }

  req.riskScore = score;
  next();
});
Enter fullscreen mode Exit fullscreen mode

まとめ

  1. リスクスコアは定量化が命 — 感覚でなく数値で判断することで、誤検知と見逃しのバランスを調整できる
  2. 5パターンの組み合わせで精度向上 — 単一指標では誤検知が多い。複合スコアで精度を高める
  3. CVによるタイミング分析 — ボットと人間を区別する強力な指標。従来のUA/IP判定を補完する
  4. Redisパイプラインで低レイテンシ — 検知処理がAPIのボトルネックにならないよう、パイプラインで一括更新

Claude Codeを使えば、これらのロジックを「脅威パターンを定義してスコアリング関数を実装して」という自然言語指示で素早くプロトタイプできる。セキュリティ設計のたたき台として活用してほしい。


Security Pack(¥1,480) — OWASP準拠のAPIセキュリティプロンプト集。不正利用検知・認証設計・脆弱性チェックリストをセットで収録。
PromptWorksで見る

Top comments (0)