この記事でできること
レート制限は「デコレータや設定を1行足せば掛かる」ものとして扱われがちですが、足しただけでは意図した回数で止まらないことがあります。しかも止まらないことは通常の利用では表面化せず、上限に当たるまで連投して初めて分かります。
この記事は、レート制限を追加した後・すでに追加済みのコードを見直すときに、そのレート制限が本当に効いているかを確認するための点検手順をまとめたものです。FastAPI + SlowAPIの構成を例にしていますが、「レート制限をハンドラのデコレータやミドルウェアで掛け、認証を別の層で行う」構成なら、フレームワークが違っても同じ点検が要ります。
前提: レート制限は「キー」と「掛かる位置」で決まる
レート制限の実装は、リクエストからキーを1つ作り、そのキーごとに時間あたりの回数を数えます。点検すべきことも2つで、キーが何によって決まるかと、レート制限のコードがリクエスト処理のどこで実行されるかです。設定した回数の数字が妥当かどうかは、この2つが正しい前提の上でしか意味を持ちません。
点検1: キーが、リクエスト送信側の自由になる値で決まっていないか
認証済みのAPIでは、IPアドレスではなくユーザー単位で数えたくなります。共有回線の裏にいる複数ユーザーを巻き添えにしないためで、方向としては妥当です。
問題は、その「ユーザー」をどう特定するかです。レート制限は認証より先に実行されることが多く、その時点ではリクエストが名乗ったトークンやIDが正しいかどうかは分かりません。未検証のトークンをハッシュ化してキーにすると、送信側は毎回でたらめなトークンを送るだけでキーを好きなだけ分散させられます。1トークンあたりの上限は守られているのに、全体としては無制限に投げられる状態になります。
点検としては、キーを作る関数が参照している値を1つずつ見て、「この値は送信側が自由に変えられるか」「変えられるとして、その時点で正しさを検証済みか」を確認します。検証済みでない値をキーに使うなら、送信側が変えられないキー(クライアントIPなど)による、より緩い上限をもう1本重ねて掛けます。ユーザー単位の細かい上限は正規利用者の公平性のため、IP単位の粗い上限はキー分散型の回避を頭打ちにするため、という役割分担にします。
点検2: 認証エラーで弾かれるリクエストが、上限にカウントされているか
ここが一番見落とされます。点検1の二段掛けを実装しても、この点検を通していないと、その二段目が一度も実行されないままになることがあります。
多くのフレームワークで、レート制限のデコレータはハンドラ関数本体をラップする形で動きます。一方で認証は、その手前の依存関係の解決やミドルウェアで行うのが一般的な書き方です。この配置だと、認証に失敗したリクエストはハンドラ本体に到達しないため、レート制限のコードも実行されません。つまり認証エラーになるリクエストは1回も数えられず、いくらでも投げられます。
「認証を通らないなら害はない」とは言えません。認証処理自体がDBアクセスやハッシュ計算を伴いますし、点検1で見た「毎回違うトークンを送る」攻撃はまさに認証エラーになるリクエストの連投です。守りたい相手が、ちょうどこの穴を通ります。
確認方法は実際に投げることです。無効なトークンを付けたリクエストと、トークンを一切付けないリクエストの2種類を、上限を超える回数だけ連投します。401や422が延々と返り続けて429にならなければ、そのレート制限は認証エラーのリクエストを数えていません。トークンの有無で処理経路が分かれる実装は珍しくないので、必ず両方試します。
点検3: 認証をハンドラ本体へ移すときの副作用を潰す
点検2に引っかかった場合の直し方は、認証をハンドラ本体の中で行うことです。依存関係では検証せずにトークンを取り出すだけにして(無ければNoneを返す)、有効性の判定と「トークン必須」の判定はどちらも本体の先頭で呼ぶ形にします。これでレート制限のデコレータが先に実行され、認証エラーのリクエストもカウント対象になります。
ただしこの変更には副作用があります。フレームワークが担っていた認証が各ハンドラの責務になるため、呼び忘れれば認証なしで通ってしまいます。「必須判定 → プロフィール解決」のような順序の取り違えも起きます。対象エンドポイントが複数あるなら、この2つをまとめて行うヘルパーを1つ用意し、各ハンドラはそれだけを呼ぶ形にします。ハンドラごとに手順を書き下ろすと、数が増えるほど確実に揺れます。
テストで固定する
この種の不具合は、正常系のテストでは絶対に検出できません。次の3つを回帰テストとして残します。
- キーを作る関数が、未検証の値を単独でキーにしていないこと(トークンが無い場合にIPへフォールバックすること)
- 無効なトークンを上限より多く連投すると429になること
- トークンを付けないリクエストを上限より多く連投すると429になること
後ろの2つは、認証の書き方をリファクタリングした瞬間に壊れます。壊れたことを教えてくれるのはこのテストだけです。
まとめ
- レート制限のキーが、送信側が自由に変えられる未検証の値で決まっていないか確認する
- ユーザー単位の上限には、送信側が変えられないキーによる緩い上限を必ず重ねる
- 認証エラーになるリクエストが数えられているかを、無効トークンとトークン未送信の2種類で実際に連投して確認する
- 認証をハンドラ本体へ移すなら、必須判定と解決をヘルパー1つへ集約し、各ハンドラは手順を書き下ろさない
この構成に行き着くまでに、同じ穴を形を変えて2回踏んでいます。1回目の修正がなぜ効いていなかったのか、なぜ気づけなかったのかは、Zenn版に書きました。