ログインなしのAPIエンドポイントセキュリティ - どこまで守るべきか

ログインなしのAPIエンドポイントセキュリティ

問題の状況

ログイン機能のない公開ウェブサイトでAPIエンドポイントを保護する必要が出てきました。

例えば、会社紹介ページでお問い合わせフォームを送信するとAPIにデータが送られますが、このAPIが外部にそのまま公開されています。ログインがないのでJWTやセッションベースの認証が使えません。

放置すると?誰かがAPIのURLを見つけて、スクリプトでフォームを何千件も送信できてしまいます。

CORSだけでは不十分

最初に思いつくのはCORS設定です。

Access-Control-Allow-Origin: https://mysite.com

これで他のドメインからブラウザでリクエストするのは防げます。でもCORSはブラウザのポリシーです。curlやPostman、サーバーサイドスクリプトからのリクエストはCORS制限を受けません。

結局CORSはセキュリティ手段ではなく便宜的な仕組みです。必要ですが、これだけでは足りません。

現実的な防御戦略

複数の方法を組み合わせて使う必要があります。

1. APIキー + HMAC署名

フロントエンドからリクエストする際にタイムスタンプと共にHMAC署名を生成して送ります。サーバーで同じ方法で署名を検証します。キーがソースコードに露出する問題がありますが、難読化と組み合わせればカジュアルな攻撃は防げます。

2. Rate Limiting

同じIPから一定回数以上のリクエストが来たらブロックします。API GatewayやNginxレベルで設定できます。

# Nginx rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=5r/m;

location /api/contact {
    limit_req zone=api burst=2 nodelay;
}

お問い合わせフォームが毎分5件以上必要なケースはないので、これでスクリプト攻撃は防げます。

3. CAPTCHA

reCAPTCHAやhCaptchaをフォームに追加します。ボットによる自動送信を防ぐ最も確実な方法です。ただしユーザー体験が悪くなるデメリットがあります。

4. ハニーポットフィールド

CSSで非表示にした入力フィールドをフォームに追加します。人間はこのフィールドが見えないので入力しませんが、ボットはすべてのフィールドを埋めようとします。このフィールドに値があればボットと判断して拒否します。

<input type="text" name="website" style="display:none" tabindex="-1">

シンプルですが意外と効果的です。

5. ワンタイムトークン

ページロード時にサーバーからワンタイムトークンを発行し、APIリクエスト時にこのトークンを一緒に送ります。一度使われたトークンは破棄します。CSRF防御と似た原理です。

私が選んだ組み合わせ

最終的に適用したのはこの組み合わせです。

  1. Rate Limiting(基本防御)
  2. ハニーポットフィールド(ボット防御)
  3. ワンタイムトークン(再利用防止)
  4. CORS(基本設定)

CAPTCHAはUXへの影響が大きいので外しました。完璧ではありませんが、ログインなしの環境で現実的に可能なレベルのセキュリティは確保できます。

100%完璧なセキュリティはありません。結局「攻撃コストを上げて面倒にする」のが核心です。