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

Cookie 認証の書き込み API に CSRF 保護を付け忘れる — DRF は『認証クラスごとに守りが変わる』

セキュリティCSRFDjangoDRF認証紡Book

Codex と Claude のレビューで浮かんだ「CSRF 保護」まわりの観察を、勉強のためにまとめたものです。

綴Photo の Web 側(Django REST Framework, 以下 DRF)のレビューで、書き込み系 API のうち1つだけ CSRF 保護が付いていない箇所が見つかった。同種の API はすべて共通の仕組みで保護していたのに、後から足したエンドポイントが1つだけその仕組みを継承し忘れていた、というパターンだった。

ここでの API は「外部からデータを読み書きするための窓口」、書き込み系 API(エンドポイント) は「その窓口のうち、データを保存・更新する側」くらいの意味で読んでほしい。今回穴が空いていたのは、その「保存・更新する窓口」の1つだ。

この記事では、実コードの細部ではなく「なぜこういう穴が空くのか」を構造として残す。

この記事で扱う観察ポイント

  • そもそも CSRF とは何で、なぜ「Cookie 認証」のときだけ問題になるのか
  • DRF では認証クラスによって CSRF の自動強制が効いたり効かなかったりする
  • 共通の防御を全エンドポイントに横展開したのに、1箇所だけ漏れる構造

ここで重要なのは、CSRF が問題になるのは Cookie を認証に使っているときだという点です。

リクエストごとに Authorization: Bearer <トークン> のようなヘッダーを明示的に付ける認証(モバイルアプリでよくある形)なら、CSRF は基本的に問題になりません。なぜなら、悪意あるページは別サイトのトークンをヘッダーに乗せられないからです(ブラウザが自動で付けるのは Cookie だけで、ヘッダーは付けてくれません)。

綴Photo の Web は「Web ブラウザは Cookie 認証、モバイルは Bearer 認証」という二刀流になっています。

JWT の置き場所。Web はブラウザ Cookie、アプリは端末。サーバ DB には無い。ブラウザJWT は Cookie access_tokenブラウザが自動で付ける合言葉は Cookie csrftoken正規画面だけヘッダーに載せるCSRF が要るモバイルアプリJWT は端末の secure storage手で Bearer ヘッダーへ付けるブラウザは自動で付けない合言葉は載せないCSRF は不要Bearer 優先、無ければ Cookie。交差であり置き場所ではない。書き込み API(同じ口)リクエストのメモリで見る。DB には無い。

コードで起きていたこと

DRF には「認証クラス」という概念があり、リクエストを誰として扱うかを決めます。綴Photo では「Bearer ヘッダーがあればそれを使い、無ければ Cookie のトークンを使う」というカスタム認証クラスを全 API のデフォルトにしていた。

つまり、どの書き込み API も Cookie 認証で叩けます。だから書き込み系には一律で「Cookie のときだけ CSRF チェック、Bearer のときはスキップ」というデコレータを dispatch(リクエストの入口)に当てる共通クラスを用意し、各 API にそれを継承させていた。

ここで「継承」とは、共通の親クラスから設定や振る舞いを”受け継ぐ”仕組みのことです。共通の見張り(CSRF チェック)を親クラスに置いておけば、そこから作られた子は自動でその見張りを引き継ぎます。

ところが、後から追加した同期用の書き込みエンドポイントが1つだけ、この共通クラスを継承せず素の API クラスを直接継承していた。

見張りはソースの親クラス。DB には無い。親 CsrfUnlessBearerMixindispatch で Cookie なら CSRFソース photos/views/mixins.py継承書き込み API 群親を継承しているCookie なら合言葉を見るソースのクラス。DB には無い。素の APIViewCSRF の見張りは無いDRF の素の入口直継承同期用の書き込み後から足して直継承したCookie POST が合言葉なしで通るソースのクラス。DB には無い。
python
# 共通の防御を継承しているエンドポイント(正しい)
class SomeWriteViewSet(CsrfUnlessBearerMixin, viewsets.ModelViewSet):
    ...
 
# 後から足したエンドポイント(CsrfUnlessBearerMixin を継承し忘れ)
class SyncSomethingView(APIView):   # ← 素の APIView 直継承
    def post(self, request):
        ...

なぜそうなりやすいか

ここがいちばんの学びだった。DRF は認証クラスによって CSRF の自動強制が効いたり効かなかったりします。

つまり、フレームワークの安全網の外側に出る設計(JWT を Cookie で運ぶ)を選んだ時点で、「CSRF 対策は自分で全エンドポイントに配る」責任が発生していた。配り方を共通クラスの継承にしていたため、継承を忘れた1箇所が静かに穴になります。「うっかり」ではなく、こういう設計だと誰でもハマる構造です。

ここまでの「認証のやり方 × CSRF の要否 × フレームワークが自動で守るか」を一覧にすると、綴Photo の置かれた位置がはっきりします。

認証のやり方 Cookie に依存? CSRF 対策の要否 DRF が自動で守る?
SessionAuthentication 依存する 要 自動で守る
JWT(Bearer ヘッダー) 依存しない 不要 (対象外)
JWT を Cookie に入れる(綴Photo) 依存する 要 守らない

最後の行だけが「対策は要るのに、フレームワークは自動で守ってくれない」という組み合わせになっています。穴が空いたのが、まさにこの位置だ。

改善の方向

直し方自体は単純で、漏れていたエンドポイントにも共通クラスを継承させるだけ。

python
class SyncSomethingView(CsrfUnlessBearerMixin, APIView):
    def post(self, request):
        ...

構造的な再発防止としては、テストで「Cookie 認証 + CSRF トークン無しの POST は拒否される(403)」を1本書いておくと、新しい書き込み API を足したときに継承忘れを機械的に検出できます。横展開した防御は「テストで一律にかける」のが、継承忘れより信頼できます。

学びポイント

  • CSRF は「Cookie で認証しているとき」だけ問題になります。Bearer ヘッダー認証なら基本不要です
  • 「Django だから CSRF は自動で守られている」は 認証クラスが SessionAuthentication のときだけ。トークン認証では自動強制は効きません
  • フレームワークの安全網の外に出る設計(JWT を Cookie で運ぶ等)を選んだら、その分の防御は自分で全エンドポイントに配る責任が生まれます
  • 「継承で防御を横展開」は、継承忘れが穴になります。一律にかかるテストで担保するほうが堅いです

このテーマの括り

今回を一言でまとめると、JWT を Cookie に入れた特殊型だから、Web 経路にだけ CSRF の「合言葉」が要ります。なのに、その合言葉チェックを書き込み API の1つで付け忘れていた、という話だった。

CSRF 保護漏れは「悪い書き方をした」というより、フレームワークがどこまで自動で守ってくれるかの境界を見誤ると起きます。SessionAuthentication の内側にいれば自動で守られ、トークン認証に出た瞬間に自前防御が要ります。境界の内と外で責任が切り替わることを意識すると、この種の穴は見つけやすくなります。


おわりに

この種の穴は「フレームワークが面倒を見てくれる範囲」と「自分で見る範囲」の境目で起きやすいです。便利なフレームワークほど「勝手に守ってくれている」と思い込みやすく、その思い込みが外れる箇所(今回なら Cookie + トークンのハイブリッド)が盲点になります。レビューで第三者(AI)に見てもらうと、こういう「前提のズレ」を拾えます。


この記事は2026年5月時点の自分の理解で書いています。Django/DRF の CSRF まわりの挙動はバージョンや設定で変わることがあるので、最新の公式ドキュメントも合わせて参照してください。