綴Photo の勉強ノートに戻る
コードレビュー学習2026.04.17

ログインを何度でも試せる状態だった — Django REST Framework のレート制限(Throttling)

コードレビューDjangoセキュリティ

コードレビューで見つかった問題を、勉強のためにまとめたものです(修正済み)。


この記事でわかること

  • 認証エンドポイントにレート制限を入れる重要性
  • DRF Throttling と django-ratelimit の使い分け

概念

レート制限(Rate Limiting)とは、一定時間内にリクエストできる回数を制限する仕組みのこと。ログイン画面は 攻撃者が最も狙いやすいエントリーポイント であり、レート制限がなければパスワードの総当たり(ブルートフォース)攻撃が成立してしまいます。

修正前のコード

LoginView、SignupView、TokenRefreshCookieView にいずれもレート制限がなかった。

python
# 修正前
class LoginView(APIView):
    permission_classes = [AllowAny]
 
    @method_decorator(csrf_protect)
    def post(self, request):
        serializer = LoginSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        username = serializer.validated_data['username']
        password = serializer.validated_data['password']
        user = authenticate(request, username=username, password=password)
        # ...
        # ← 何度でも試せる。失敗してもロックなし

なぜ危険か

  • パスワード総当たり: 1 秒間に数千リクエスト送信可能。よくあるパスワードを全試行されます
  • アカウント大量作成: SignupView が無制限なのでスパムアカウントを大量生成できます
  • トークン消耗攻撃: TokenRefreshView を叩き続けてブラックリストを消耗させられます

対策

綴Photo では DRF の ScopedRateThrottle を使い、認証エンドポイントごとに別々のレートを割り当てた。一律ではなく、ログインは厳しめ・トークン更新は緩め、とエンドポイントの性質に合わせて分けられるのが利点。

レート制限のやり方は大きく3つあり、どこまで細かく絞りたいかで選びます。

回数はキャッシュに乗る。DB のテーブルには保存しない。AnonRateThrottle全エンドポイント一律最小コストの蓋向く: とりあえず同じ上限anon 10/min などScopedRateThrottleエンドポイントごとに別レート綴Photo はこれログイン 10/min・登録 5/minトークン更新 60/mindjango-ratelimitIP・ユーザー名単位追加パッケージ向く: 上2つでは届かない粒度key=ip で 5/m など左から右へ細かくなる。綴Photo は真ん中で足りると判断した。追加パッケージは入れてない。
python
# settings/base.py — レートを「定義」する場所。scope 名ごとに上限を決める
REST_FRAMEWORK = {
    # ScopedRateThrottle を使う宣言(scope 別にレートを分けられる方式=上の図の真ん中)
    'DEFAULT_THROTTLE_CLASSES': ['rest_framework.throttling.ScopedRateThrottle'],
    'DEFAULT_THROTTLE_RATES': {
        'auth_login': '10/min',          # ← この scope 名が View 側と対応する
        'auth_signup': '5/min',
        'auth_token_refresh': '60/min',
    },
}
python
# views.py — レートを「適用」する場所。View に scope を貼って効かせる
from rest_framework.throttling import ScopedRateThrottle
 
class LoginView(APIView):
    throttle_classes = [ScopedRateThrottle]   # この View で throttle を有効化
    throttle_scope = 'auth_login'             # ← settings の 'auth_login'(10/min) と名前で紐づくから効く
    # ...

django-ratelimit のような追加パッケージを入れなくても、scope を分けるだけでエンドポイント単位の制御ができます。

キャッシュの注意: DRF の throttle はカウントをキャッシュに持つ。

上限の定義は設定ファイル。回数の実体はプロセス内メモリ。DB には保存しない。設定ファイルsettings/base.pyDEFAULT_THROTTLE_RATES上限の定義。回数ではないauth_login 10/min など今の本番LocMemCacheプロセス内メモリ回数の実体再起動で消える。共有されないサーバ DB回数は乗らないthrottle の行は無いテーブルには書かない書いていないので残らないView の throttle_scope が同じ名前で結ぶ。関所であり、置き場所ではない。綴Photo は現状 LocMem 運用のまま。複数プロセスでは箱が分かれる。Redis は規模が要求するまで保留。厳密に効かせるならキャッシュを Redis へ向ける。

学びポイント

  • 認証エンドポイントのレート制限は「機能を作ったらセットで入れる」もの。後回しにするほど対応コストが上がります
  • DRF の Throttling は設定数行で入ります。まず ScopedRateThrottle で全体に入れて、エンドポイントごとに scope でレートを調整するのがおすすめ
  • AllowAny なエンドポイントほど攻撃されやすいです。パーミッションと合わせてスロットリングをデフォルト設定にします


おわりに

レート制限なしの認証エンドポイントは、機能ゼロから実装した直後に最もよく発生する脆弱性。「ログイン作ったら忘れずレート制限」をチェックリストに入れます。


この記事は2026年4月時点の自分の理解で書いています。django-ratelimit や DRF Throttling の設定方法は変化が速い領域なので、最新の公式ドキュメントも合わせて参照してください。