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

1回ぶんを守るだけでは足りない — 公開エンドポイントに『どこで線を引くか』

セキュリティレートリミットバリデーションDjangoDRF紡Book

Codex と Claude のレビューで浮かんだ「上限と検証」まわりの観察を、勉強のためにまとめたものです。一部は指摘を受けて修正済み、一部は今後の課題として残っています。

外部に公開する API は、悪意ある人だけでなく「事故」からも守る必要があります。巨大なファイル、巨大なテキスト、総当たり——これらに対して「どこに上限を引くか」は、機能を作るときに見落としやすいです。綴Photo の Web レビューで、上限まわりの指摘が複数出たので、テーマでくくってまとめる。

この記事で見ていく点

  • リクエスト単位の上限はあるが、ユーザー単位の累積上限・レート制限が無い(今後の課題)
  • テキスト項目に長さ上限が無く、巨大 payload(リクエストで送られてくるデータ本体)を受け入れてしまっていた(対応済み)
  • 共有パスワードが弱かった(最小6文字・複雑性なし)が、総当たりは別の仕組みで抑えられている(最小長は対応済み)

アップロードの「1回の上限」はあるが「累積の上限」が無い(今後の課題)

綴Photo のアップロードは、前者(1回の上限)がしっかり実装されていた。1ファイルのサイズ、1リクエストの合計サイズ、画像のピクセル数、それぞれに上限があります。

コードで起きていたこと

足りていないのは「累積」側だ。1回のアップロードは厳しく制限されているが、同じ認証済みユーザーが繰り返しアップロードする総量には歯止めがない。

python
# 1回あたりの上限はある(良い)
PHOTO_UPLOAD_MAX_FILE_SIZE  = 20MB   # 1ファイル
PHOTO_UPLOAD_MAX_TOTAL_SIZE = 100MB  # 1リクエスト合計
PHOTO_UPLOAD_MAX_PIXELS     = 40M    # 1画像のピクセル数
 
# レートリミットの枠(抜粋)— ログインや共有ページ検証にはあるが
# アップロード用のスコープが無い
DEFAULT_THROTTLE_RATES = {
    "shared_page_verify": "10/min",
    "auth_login": "10/min",
    # "photo_upload": ... ← 無い
}
1回の線は設定とメモリ。通った写真は S3。累積を数える箱は DB にもキャッシュにも無い。設定ファイルsettings/base.py20MB / 100MB / 40M1回の上限の定義画像は無い。DB には書かない通った写真本番 S3Photo.image1回通るたびに増える実体はストレージサーバ DB合計バイトの欄は無いPhoto 行は増えるユーザー容量は数えないクォータ用の表は無い1回の照合はリクエスト中のプロセスメモリ。保存しない。回数の枠 photo_upload は LocMemCache にも無い。あるのは shared_page_verify と auth_login。累積の線は、容量なら DB、回数ならキャッシュ。どちらもまだ箱が無い。

なぜそうなりやすいか

「1回のリクエストを守る」ことに意識が向きやすいからです。フォームやシリアライザを書くときは、目の前のリクエストを検証します。一方「同じ人が何百回も叩いたら」という累積の視点は、コードを書いている瞬間には見えにくいです。特に個人開発では「自分しか使わない」という前提が働きやすく、累積制限は後回しになりやすいです。

今後どうするか(これは保留事項)

ここは公開直後から必須というより、実トラフィックを観測してから調整でも間に合う種類の防御だと考えている。「枠だけ用意して数字は後で詰める」のが現実的なので、今後の対応として残している。

  • アップロード/同期エンドポイントに、時間あたりの回数制限(レートリミットのスコープ)を足す
  • ストレージ課金(容量に応じた料金)を入れるなら、ユーザー単位の累積容量クォータ(1人あたりの総量上限)も設ける

学びポイント

  • アップロードの上限は「1回」と「累積」で別物。1回の上限を固めても、累積(総量・回数)が無いとコストを膨らませられます
  • decompression bomb 対策はファイルサイズではなくピクセル数で見ます
  • レートリミットは「ログイン」だけでなく「書き込み・アップロード」にも要ります

テキストに長さ上限が無かった(対応済み)

巨大なファイルだけでなく、巨大なテキストも同じ問題を起こします。レビュー時点では、綴Photo の本文や同期データのテキスト項目に、実質的な長さ上限が設定されていなかった。

なぜ危険か

上限が無いと、攻撃者は1リクエストで数MBの文字列を送り込めます。問題は、それが書き込みは1回でも、読み出しは何度も起きることです。

書き込みは1回。残る場所は DB の Meaning.text。読み出しも同じ行である。届いた本文HTTP の bodyプロセスメモリ1リクエストで数MBも届くこの段階では保存しないサーバ DBMeaning.textTextField列に長さ上限は無い書いた行は残り続ける返す応答同じ Meaning 行の写しメモリ。保存しない何度でも全量を積む第三の箱は作らないmax_length=5000 は serializer の関所で、置き場所ではない。同期 items の 500 件も同じ関所。件数の実体はリクエストの配列で、保存前に切る。

どう直したか

文字を入れる箱(フィールド)に「ここは最大◯◯文字まで」という札を貼ります。Django では serializer(外から来たデータを DB 保存前に検品する部品)の CharField に max_length を指定します。

python
# 修正前: 長さ上限が無い
text = serializers.CharField(required=False, allow_blank=True, default="")
 
# 修正後: 業務上現実的な上限を付けた(本文は 5000 文字)
text = serializers.CharField(required=False, allow_blank=True, default="", max_length=5000)

配列で複数件を受け取る項目(同期の items など)も同じで、件数の上限を付けておかないと「1リクエストに数万件」が通ってしまいます。こちらも1リクエスト最大 500 件に制限した。

学びポイント

  • 入力の「大きさ」を制限するのは、ファイルサイズだけでなくテキストの長さ・配列の件数も同じ
  • フォームの見た目には現れない項目(API の生データ)こそ上限を忘れやすいです
  • 「サイズ」はファイルだけにあるものではありません。入力欄を作ったら、長さの上限も一緒に決める癖をつけます

共有パスワードは弱かったが、総当たりは別の仕組みで抑えられている(最小長は対応済み)

ここは「AI レビューの指摘を実コードで裏取りすると、評価が変わる」良い例だった。

綴Photo には、写真をまとめたページをパスワード付きで共有する機能があります。このパスワードの設定が、レビュー時点では「最小6文字・複雑性チェックなし」だった。レビューでは当初「試行回数の制限が無いので総当たりされうる」という指摘が出た。

ところが実コードを確認すると、パスワード検証エンドポイントには『1分10回まで』のレートリミットが効いていた。つまりオンラインの総当たりは1分10回に制限されていて、「試行制限が無い」は不正確だった。パスワード自体も適切にハッシュ化して保存されています。

python
class SharedPagePublicView(APIView):
    throttle_classes = [ScopedRateThrottle]
    throttle_scope = "shared_page_verify"   # 10/min。総当たりはここで抑えられる

最小長は、指摘を受けて6文字から8文字に引き上げた(弱いパスワードを設定できる入口を狭めた)。検証側にも巨大入力を弾く上限(128文字)を付けて、長すぎる文字列でハッシュ計算を重くされる事故も防いでいます。

レビューが空だとした回数の箱は、キャッシュに実在した。平文は残さない。届いた合言葉HTTP の bodyメモリ・保存しない発行でも検証でも同じ平文のままでは残さないキャッシュLocMemCache送信元 IP あたり 10/min回数の実体。DB には無いプロセス内メモリサーバ DBSharedPagepassword_hashハッシュだけ残る閉じても残る最小8・最大128は発行の関所で、置き場所ではない。複雑性を見る箱は無い。回数の単位は IP であり、ユーザーではない。「制限あり」と「弱パスワードを許す」は別の線である。

ただし、残る弱点はあります。

  • 8文字でも単純な文字列は許してしまいます
  • IP 単位で数える方式には原理的な限界がある(同じ仕組み全般に言える話で、本気の総当たりまでは止めきれません)
  • 共有先は写真の公開 URL になりうるので、パスワード強度は守る対象の重さに見合っている必要があります

学びポイント

  • AI レビューの指摘は鵜呑みにせず、実コードで裏取りすると評価が変わることがある(「試行制限なし」→ 実際は 10/min あった)
  • レートリミットは「何単位(IP / ユーザー / トークン)で数えているか」で強さが変わります。単位を意識しないと「制限あり」が実質ザルになりえます
  • 「総当たりは抑えられている」と「弱パスワードを許す」は別問題。レートリミットがあっても、最小長・複雑性の入口を緩くしていいわけではありません

このテーマの括り

3つとも「外から入ってくるものに、どこで線を引くか」の問題です。ファイルの大きさ、テキストの長さ、試行の回数、パスワードの強度——どれも「1回のリクエスト」を見るだけでは足りず、「繰り返し」「累積」「弱い入力を許すか」まで含めて線を引く必要があります。公開エンドポイントは、悪意だけでなく事故(巨大ファイルの誤送信など)からも守る前提で設計すると、引くべき線が見えてきます。


おわりに

「上限」は、機能が動くかどうかには関係しないので、作っている最中は後回しになりやすいです。だが公開した瞬間に、上限の無さは「コストを膨らませられる穴」「思い出が漏れる穴」に変わります。この種の穴は「1回ぶんだけ見て、繰り返し・累積・読み出し回数を見ていない」ときに生まれやすいです。

一方で、すべてを公開前に完璧に詰める必要もなく、「枠だけ用意して数字は実トラフィックで調整」できるものも多い(今回の累積上限がまさにそれだ)。どれが公開前マストで、どれが後で足せるかを見分けるのも設計の一部だと感じた。


この記事は2026年5月時点の自分の理解で書いています。レートリミットやアップロード上限の最適値は運用実態で変わるので、数字は参考程度に。