「リンクを踏んだだけ」でデータが消える設計
新人が管理画面の「ユーザー削除」機能を実装しました。
<a href="/admin/delete?id=5">このユーザーを削除</a>
@app.get("/admin/delete")
def delete_user(id: int):
db.delete_user(id)
return RedirectResponse("/admin/users")
<a> タグでリンクを作り、GETリクエストで削除APIを叩く。画面から削除ボタンを押せばユーザーが消える。一見、正常に動きます。
しかしこの設計には、3つの致命的な問題が潜んでいます。
問題1:ブラウザのプリフェッチが削除を発動する
モダンブラウザには、ページ内のリンクを先読み(プリフェッチ)してページ表示を高速化する機能があります。つまり、ユーザーが管理画面を開いただけで、ブラウザが勝手に /admin/delete?id=5 にGETリクエストを飛ばし、ユーザーが何も押していないのにデータが消える可能性があります。
問題2:検索エンジンのクローラーが全削除する
もし管理画面にBasic認証すらかかっていなかった場合、Googleのクローラー(Googlebot)がリンクを辿って /admin/delete?id=1、/admin/delete?id=2、/admin/delete?id=3…と順番にアクセスし、全ユーザーが自動削除されるという悪夢が起こりえます。(実際にこの事故は過去に複数のサービスで報告されています)
問題3:CSRF攻撃に対して完全に無防備
攻撃者が悪意のあるサイトにこう書くだけで、管理者がそのサイトを踏んだ瞬間にデータが消されます。
<!-- 攻撃者のサイト -->
<img src="https://target.com/admin/delete?id=5" />
<!-- ブラウザは画像を取得しようとGETリクエストを送る → 削除が発動 -->
HTTPメソッドの正しい使い分け
┌──────────┬───────────────────────────────┐
│ メソッド │ 用途 │
├──────────┼───────────────────────────────┤
│ GET │ データの取得(副作用なし) │
│ POST │ データの作成 │
│ PUT/PATCH│ データの更新 │
│ DELETE │ データの削除 │
└──────────┴───────────────────────────────┘
【鉄則】
GETリクエストは「冪等(何回叩いても結果が同じ)」であり、
サーバーの状態を変更してはいけない。
正しい実装
<!-- フォームでPOSTリクエストを送る -->
<form method="POST" action="/admin/delete">
<input type="hidden" name="id" value="5" />
<input type="hidden" name="csrf_token" value="{{ csrf_token }}" />
<button type="submit" onclick="return confirm('本当に削除しますか?')">
削除
</button>
</form>
@app.post("/admin/delete") # GETではなくPOST
def delete_user(id: int, csrf_token: str = Form(...)):
verify_csrf_token(csrf_token) # CSRF対策
db.delete_user(id)
return RedirectResponse("/admin/users")
GETで状態を変更するコードをレビューで見つけたら、いくら動いていても即修正を依頼すべきです。これは「ベストプラクティス」ではなく「HTTP仕様違反」であり、予測不能な事故を引き起こす時限爆弾です。