はじめに
- Session Fixation(セッション固定) は、攻撃者があらかじめ用意したセッションIDを被害者に使わせ、被害者がログインした後もそのIDが認証済みのままになることで攻撃者がなりすます攻撃です。
- 対策は 「ログイン成功時に必ずセッションIDを再生成(regenerate/rotate)する」 こと。
- 本記事では
flask-session-demo(教育用サンプル)を使って、脆弱モードでの再現手順(curl コマンド)と安全モードでの対照実験を示します。
1. セッション固定とは
攻撃者が事前に作成・知っているセッションID(例:attacker123)を被害者に使わせ、被害者がログインしてもサーバがセッションIDを再生成しなければ、その attacker123 は認証済みとなり、攻撃者が同じ ID でログイン済み操作を実行できるようになる攻撃です。
2. 攻撃の流れ
- 攻撃者が
attacker123を用意する。 - 被害者を誘導してその
sessionを使わせる(Link、Set-Cookie、誘導ページ等)。 - 被害者がそのセッションでログインする。
- サーバがセッションIDを再生成しない →
attacker123が認証済みになる。 - 攻撃者は
attacker123を使って不正操作を行う。
3. 環境:flask-session-demo の前提
- リポジトリ構成は本文の中で説明した
flask-session-demo(教育用)を想定。 -
login_fixationエンドポイントが「受け取ったsidをそのままSet-Cookieする」デモ実装になっている。 -
transferエンドポイントはログイン済みユーザのみ許可される簡易 API (デモ用)。
Source Code
https://github.com/sayhellotogithub/pythontest/tree/main/project/flask-session-demo
4. 実演:脆弱モードでの再現手順(curl)
以下は 脆弱モード(ログイン時のセッション再生成をスキップ)で起動して実行する手順です。
1) サーバ起動(脆弱モード)
# プロジェクトルートで
INSECURE_NO_HTTPONLY = os.getenv("INSECURE_NO_HTTPONLY", "true").lower() == "true"
INSECURE_NO_SECURE_COOKIE = os.getenv("INSECURE_NO_SECURE_COOKIE", "true").lower() == "true"
INSECURE_NO_SAMESITE = os.getenv("INSECURE_NO_SAMESITE", "true").lower() == "true"
INSECURE_SKIP_SESSION_REGEN_ON_LOGIN = os.getenv("INSECURE_SKIP_SESSION_REGEN_ON_LOGIN", "true").lower() == "true"
INSECURE_XSS_COMMENT = os.getenv("INSECURE_XSS_COMMENT", "true").lower() == "true"
python3 app.py
# あるいは docker compose up --build (docker-compose.yml に env を設定)
サーバは http://localhost:5001 を想定します(ポート違う場合は適宜読み替え)。
2) Victim(被害者)が攻撃者仕込みの SID でログイン
(ここで attacker123 を攻撃者が用意したセッションIDとする)
curl -i -c victim_cookies.txt -X POST \
-d "sid=attacker123&username=alice&password=password123" \
http://localhost:5001/login_fixation
-
-c victim_cookies.txtにSet-Cookieが保存されます。脆弱モードならsession=attacker123が入っているはずです。
確認:
cat victim_cookies.txt
# (Set-Cookie ヘッダを確認して cookie 値が attacker123 になっているか見る)
3) Attacker(攻撃者)が同じセッションで認証操作を実行
curl -i -b "session=attacker123" -X POST \
-d "to=attacker&amount=1000" \
http://localhost:5001/transfer
期待挙動(脆弱):
HTTP/1.1 200 OK
...
Transferred 1000 to attacker by alice
→ 攻撃者は alice の権限で transfer を実行できてしまう。
5. 対照実験:安全モードでの挙動確認
サーバを止めて(Ctrl+C)以下で再起動します:
INSECURE_NO_HTTPONLY =False
INSECURE_NO_SECURE_COOKIE =False
INSECURE_NO_SAMESITE = False
INSECURE_SKIP_SESSION_REGEN_ON_LOGIN =False
python3 app.py
同じ手順(Victim が /login_fixation に sid=attacker123 を送る → Attacker が session=attacker123 を使う)を実行すると、攻撃者は認証済み操作を実行できないはずです(401 や login required、または被害者のセッションが別 ID に置き換わっている)。
curl -i -b "session=attacker123" -X POST \
-d "to=attacker&amount=1000" \
http://localhost:5001/transfer
HTTP/1.1 401 UNAUTHORIZED
Server: Werkzeug/3.1.3 Python/3.13.0
Date: Wed, 17 Sep 2025 13:19:45 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 14
Connection: close
6. なぜセッション再生成が効くのか(短い理由)
ログイン成功時に既存の(攻撃者が与えた)セッションを破棄して新しいランダムなセッションIDを発行すれば、攻撃者が事前に知っていた ID は無効になります。つまり「攻撃者が使おうとしているIDはログイン後は使えない」ため、固定攻撃が成立しません。
7. 検出・対策チェックリスト(運用)
- ログイン成功時にセッションID を再生成しているか?(必須)
- 認証 cookie に
HttpOnly; Secure; SameSite=Laxが設定されているか? - セッションID を URL パラメータに載せていないか?
- サーバ側ログでログイン前後の session ID の変化を監査しているか?(検知用)
- 同一 session が短時間に複数 IP / 複数ユーザで使われていないか監視する(異常検出)
- 必要ならログイン後の IP / UA 変更があれば再認証やセッション破棄を行う
8. 付録:ワンコマンドで試す(脆弱モードが立っている前提)
# Victim logs in with attacker-supplied SID
curl -s -c victim_cookies.txt -X POST -d "sid=attacker123&username=alice&password=password123" http://localhost:5001/login_fixation
# Attacker reuses known SID to call transfer
curl -i -b "session=attacker123" -X POST -d "to=attacker&amount=1000" http://localhost:5001/transfer
まとめ
- Session Fixation は比較的単純だが危険な脆弱性。ログイン時のセッションID再生成が最も重要な対策。
-
flask-session-demoのような教育用サンプルで「脆弱モード ↔ 安全モード」を切り替えながら違いを確認すると理解が深まります。 - 本番環境では cookie の属性やサーバ側のセッション管理(ストアや取り消し)も含めて包括的に設計してください。