Skip to main content
NAOKI KANEKO

お問い合わせのレート制限を、外部ストアなしで実装した理由


お問い合わせのレート制限を、外部ストアなしで実装した理由
#Security#Next.js

このサイトのお問い合わせフォームは、メールを送る。連打されると、迷惑メールの踏み台になったり、メールサービスの送信上限を使い切ったりする。管理画面(Decap CMS)のログイン処理も、総当たりの対象になりうる。

そこで、レート制限を入れた。この記事では、外部のストアを使わない方式を選んだ理由と、その方式の限界を書く。判断の記録は ADR-0003 にある。

何をしているか

  • クライアントは、x-forwarded-for の先頭のアドレスで識別する。なければ x-real-ip、どちらも取れなければ共通の unknown とする。
  • 固定窓のカウンタを Map に持つ。お問い合わせは、IP ごとに 10 分で 5 回まで。管理画面の OAuth は、IP ごとに 10 分で 10 回まで。キーに接頭辞を付けて、2 つの枠を混ぜない。
  • 超えると 429 を返す。画面側は、送信失敗として表示する。

なぜ、外部のストアを使わなかったか

サイトは小規模で、データストアを持っていない。Redis や Upstash を足すと、外部サービスの契約と設定が増える。通常の利用は、数回の送信で終わる。この規模には、過剰だと判断した。

CAPTCHA も使っていない。利用者の負担が増えるためで、今は honeypot(人間には見えない入力欄)とレート制限、本文の大きさの上限(16KB)、入力の検証、メール側の送信上限で、多重に守っている。

限界

この方式には、はっきりした弱点がある。

  • サーバーレスの環境では、インスタンスごとにカウンタが別になる。コールドスタートでも消える。複数のインスタンスに分散した攻撃は、完全には止められない。
  • x-forwarded-for は、信頼できるプロキシ(このサイトでは Vercel)の背後でだけ、正しい値になる。

どちらも、実際に濫用が見られたときに困る。そうなった場合は、共有ストアに移すと決めてある。checkRateLimit の呼び出し側は変えずに、実装だけを差し替えられる形にしてある。

次にやること

共有ストアへの移行は、Issue として起こしてある。契約や費用が絡むので、実際に濫用が見られるか、アクセスが増えてから進める。それまでは、この方式の限界を、ADR に書いたまま運用する。