flawopen.com/Vulnerabilities/セッション固定とCookieの保護
セッション固定:ログイン時にIDを更新する(CWE-384)
攻撃者がすでに知っている手荷物引換番号を、旅行者が使うよう仕向けられたと想像してください。航空会社が預け入れ後も同じ番号を維持すると、攻撃者が荷物を受け取れるかもしれません。セッション固定では、攻撃者が被害者に使わせられる識別子をアプリが受け入れ、ログイン後も更新せずに認証済みセッションへ昇格させます。両方の条件が必要です。アプリがその識別子を受け入れたり設定させたりできない限り、通常のフィッシングリンクだけではセッションを固定できません。
主要概念とサブシステム用語
- セッション固定
- アプリが攻撃者に知られた識別子を受け入れ、被害者の認証後も維持するセッション管理の不備です。
- セッションIDの更新
- 認証によってセッションの権限が上がるとき、新しく予測困難な識別子を発行します。アプリが維持する必要のあるセッションデータだけを引き継ぎます。
- Cookie属性
SecureはHTTPSでのCookie送信に制限し、HttpOnlyはJavaScriptによる読み取りを防ぎ、SameSiteは一部のサイト間送信を制御します。それぞれ異なるリスクに対処し、ID更新の代わりにはなりません。__Host-プレフィックス- 対応ブラウザーでは、HTTPS経由で設定し、Secure、
Path=/を指定し、Domain属性を付けないことが求められます。兄弟サブドメインが親ドメイン向けの同名Cookieを設定することを防ぐ助けになります。
攻撃の流れを段階ごとに解説
攻撃者がセッション識別子を得る、または選ぶ
別の脆弱性や機能によって、攻撃者がIDを取得し、被害者のブラウザーに送信させます。経路はアプリによって異なります。URL内のセッションIDを受け付けない場合、通常のフィッシングリンクだけでは成立しません。
被害者が攻撃者の知る識別子を使う
ブラウザーはそのIDをアプリに送り、アプリは有効な匿名セッションとして扱います。攻撃者は同じIDを識別または再利用できる必要があります。
認証によってセッション権限が上がる
被害者がログインします。アプリがIDを更新せず既存IDに認証済みの本人情報を結び付けると、既知の識別子が認証済みセッションを指すようになります。
攻撃者が変わっていないIDを再利用する
攻撃者が引き続きIDを送信でき、サーバーが無効化していなければ、被害者のセッションにアクセスできる可能性があります。権限変更時のID更新でこの連鎖を断てます。
仕組みと根本原因
セッション固定には、攻撃者が被害者の使う識別子に影響を与えられることと、ログイン成功時にアプリがそれを置き換えないことの両方が必要です。Cookie属性は多層防御になりますが、ID更新の代わりにはなりません。HttpOnlyはスクリプトからの読み取りを制限し、SecureはHTTPSを要求し、SameSiteは一部のサイト間送信を減らし、__Host-は対応ブラウザーでCookieの適用範囲を制限します。
# VULNERABLE: custom session code keeps the pre-authentication ID.
def login_view(request):
user = authenticate(request, request.POST["username"],
request.POST["password"])
if user is not None:
# BUG: privilege changes, but the session ID does not.
request.session["user_id"] = user.pk
request.session["authenticated"] = True
return redirect("/account/")
return redirect("/login/")
# HARDENED: Django login() rotates the session key on authentication.
from django.contrib.auth import authenticate, login
def login_view(request):
user = authenticate(request, request.POST["username"],
request.POST["password"])
if user is not None:
# Uses Django's supported authentication/session integration.
login(request, user)
return redirect("/account/")
return redirect("/login/")
# settings.py: use HTTPS, host-only scope, and a deliberate SameSite policy.
SESSION_COOKIE_NAME = "__Host-sessionid"
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_SAMESITE = "Lax"
SESSION_COOKIE_PATH = "/"
SESSION_COOKIE_DOMAIN = None
攻撃シーケンスと概念例
- 1前提条件: 別の脆弱性や機能により、被害者のブラウザーが送信するIDを攻撃者が操作できます。
- 2既知のID: 攻撃者が同じ匿名セッションIDを識別または再利用できます。
- 3認証: ブラウザーが既知のIDを送り続けたまま被害者がログインします。
- 4ID未更新: アプリは識別子を置き換えず、既存セッションの権限を上げます。
- 5条件付きの影響: 既知IDが有効で使用可能な間に限り、攻撃者は認証済みセッションへアクセスできます。
エンジニア向け防御・予防ルール
- ✓権限変更時にIDを更新する: ログインなどの権限昇格時に予測困難な新IDを発行し、フレームワークの仕様に沿って以前のIDを無効にします。
- ✓フレームワークの認証APIを使う: IDを手動でセッションに書き込まず、サポートされたログイン/セッション連携(Djangoでは
django.contrib.auth.login())を使用します。 - ✓攻撃者が制御するIDを拒否する: URL内のセッションIDを受け入れないでください。兄弟サブドメインからのCookie注入を防ぎ、独自のCookie・セッション処理をテストします。
- ✓Cookie属性を用途に合わせる:
SecureとHttpOnlyを使用し、Path=/かつDomainなしの__Host-名を検討します。正当なサイト間ログインに合わせてSameSite=LaxまたはStrictを選びます。必要なCSRFトークンをSameSiteで置き換えないでください。 - ✓遷移を検証する: 自動テストで匿名IDを記録し、認証後にIDが変わったことと、旧IDで認証済みセッションへアクセスできないことを確認します。