紡Book の勉強ノートに戻る
コードレビュー学習2026.04.09

認証不要の共有ページを総当たりから守る3つの層

コードレビューセキュリティレートリミットDjangoDRF

コードレビューで「認証不要の共有ページをどう守るか」を整理したときのメモを、勉強のためにまとめたものです。


この記事でわかること

  • 認証不要の公開エンドポイントに必要な防御の考え方
  • レートリミット(DRF Throttle)・ハッシュ保存・最低文字数の組み合わせ方
  • ハッシュ照合が「タイミング攻撃」にも強い理由

概念

綴Photo には、写真をまとめたページをパスワード付きで共有する機能があります。これは「ログイン不要・URL(共有トークン)を知っていれば誰でも開ける」公開エンドポイントです。パスワード(アクセスコード)の検証を行うので、総当たり攻撃の標的になりやすいです。

綴Photo はこの公開エンドポイントを、3つの層で守っています。

置き場所は3つ。合言葉の平文はメモリにしか無く、保存しない。届いた合言葉HTTP の bodyメモリ・保存しない発行でも検証でも同じ平文のままでは残さないキャッシュLocMemCachethrottle カウンタ送信元あたり 10/minプロセス内メモリサーバ DBSharedPagepassword_hashハッシュだけ残る閉じても残る発行: 左から右へ、最低8・最大128文字を通してハッシュを書く。検証: 左から中央へ回数を1足す。通ったら右のハッシュと照合する。文字数チェックと照合は関所で、置き場所ではない。

守り①: レートリミット(DRF の ScopedRateThrottle)

綴Photo は Django REST framework(DRF)の ScopedRateThrottle を使い、共有ページ検証エンドポイントに専用の枠(10/min)を割り当てています。

python
from rest_framework.throttling import ScopedRateThrottle
 
class SharedPagePublicView(APIView):
    permission_classes = []        # 認証不要(公開)
    authentication_classes = []
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = "shared_page_verify"   # 下の設定で 10/min
 
# settings
DEFAULT_THROTTLE_RATES = {
    "shared_page_verify": "10/min",
    # ... 他のスコープ
}

本番運用での注意が1つあります。Throttle のカウンタはキャッシュに乗ります。

回数の置き場所で、効き方が変わる。箱がプロセスの外に出ないと、数えが分かれる。今の本番LocMemCacheプロセス内メモリ箱は1つ10/min どおりプロセスが増えたときLocMemCache が N 個箱がプロセスごとに分かれる数えが共有されない実効は N × 10/min切替先Redis全プロセスで1つの箱プロセスをまたぐ10/min のままローカルで効いていることと、本番の箱が1つかは別である。回数は DB の SharedPage には乗らない。キャッシュの寿命で消える。

綴Photo は当面 Render を1インスタンス+プロセス内メモリキャッシュ(LocMemCache)で運用し、スケールしたら共有キャッシュ(Redis)に切り替える、という判断を設定にコメントで残しています。

守り②: パスワードはハッシュで保存する(make_password / check_password)

共有ページのパスワードは「ただのアクセスコードだから平文で持っても…」と迷いやすいところです。

python
from django.contrib.auth.hashers import make_password, check_password
 
# 発行時: ハッシュ化して保存
shared.password_hash = make_password(raw_password)
 
# 検証時: ハッシュ照合
if not check_password(raw_password, shared.password_hash):
    return Response({"detail": "パスワードが違います。"}, status=400)

共有先は写真の公開 URL になりうる=守る対象が重いです。アクセスコード的な位置付けでも、平文で持つ理由はありません。

守り③: 最低文字数とサイズ上限

python
class SharedPageCreateSerializer(serializers.Serializer):
    password = serializers.CharField(write_only=True, min_length=8, max_length=128)
  • min_length=8: 短いコードはレートリミットがあっても総当たりの余地が残るので、入口で下限を引きます
  • max_length=128: 長すぎる文字列を送りつけてハッシュ計算を重くされる事故(DoS)を防ぐ上限です

タイミング攻撃について

「パスワード比較」と聞くと、== で比べたときのタイミング攻撃が気になるかもしれません。

ここは、守り②でハッシュ照合に切り替えた時点で解決しています。check_password は内部で定時間比較を行うため、平文を == で比べたときのような応答時間リークは起きません。自前で hmac.compare_digest を呼ぶ必要はありません(綴Photo では Webhook トークンの検証など、別の箇所で compare_digest を使っています)。

なぜ守り漏れが起きやすいか

公開エンドポイントの守りが抜けるのには、構造的な理由があります。ログイン処理のエンドポイントには反射的にレートリミットや文字数制限を入れるのに、「共有ページのパスワード照合」は "認証じゃない・ただの一致チェック" と脳内で分類されやすいです。その瞬間、防御の意識からすっぽり抜けます。

公開=認証なしのエンドポイントこそ、ログインの壁がない分、守りを「自分で明示的に設計する」必要があります。「これは認証じゃないから」と分類したエンドポイントが、いちばん盲点になりやすいです。

学びポイント

  • どの層も単体では穴が残ります。3つをセットで設計します
  • 公開 = 誰でもアクセスできる = 攻撃者も同じ立場です。「URL を知られなければ大丈夫」は防御ではありません
  • ハッシュ照合(check_password)は定時間比較なので、平文 == の応答時間リークも同時に塞げます。「ハッシュにする」は保存の安全だけでなく比較の安全もついてきます
  • 「認証じゃない、ただの一致チェック」と分類したエンドポイントが盲点になりやすいです

おわりに

公開エンドポイントは「攻撃者と同じ立場の人が常に叩いている」前提で設計します。それぞれ単体では完璧ではないが、組み合わせて初めて「割に合わない攻撃対象」になります。

レートリミットは本番でカウンタの共有(キャッシュバックエンド)が要ること、ハッシュ照合なら定時間比較が無料でついてくること——どこで何を守っているかを一度棚卸しすると、抜けが見えます。


この記事は2026年4月時点の自分の理解で書いています。レートリミットの設定や本番運用の注意点は変化が速い領域なので、最新の公式ドキュメントも合わせて参照してください。