FlawOpen

flawopen.com/Vulnerabilities/セッション固定とCookieの保護

セッション固定:ログイン時にIDを更新する(CWE-384)

High Severity CWE-384 Cryptography & Auth
💡 やさしい説明 — 貼り付けられた手荷物引換券

攻撃者がすでに知っている手荷物引換番号を、旅行者が使うよう仕向けられたと想像してください。航空会社が預け入れ後も同じ番号を維持すると、攻撃者が荷物を受け取れるかもしれません。セッション固定では、攻撃者が被害者に使わせられる識別子をアプリが受け入れ、ログイン後も更新せずに認証済みセッションへ昇格させます。両方の条件が必要です。アプリがその識別子を受け入れたり設定させたりできない限り、通常のフィッシングリンクだけではセッションを固定できません。

対象: サーバー側のセッション管理と、認証済みブラウザーが使う識別子。
前提条件: 攻撃者が、受け入れられるURL内IDやCookie注入などを通じて、既知のセッションIDを被害者に使わせられること。
不備: 匿名セッションから認証済みセッションへの移行時に、アプリが攻撃者に知られたIDを維持すること。
対策: 認証時に予測困難な新しいIDを発行し、脅威ごとにCookie属性を設定します。

主要概念とサブシステム用語

セッション固定
アプリが攻撃者に知られた識別子を受け入れ、被害者の認証後も維持するセッション管理の不備です。
セッションIDの更新
認証によってセッションの権限が上がるとき、新しく予測困難な識別子を発行します。アプリが維持する必要のあるセッションデータだけを引き継ぎます。
Cookie属性
SecureはHTTPSでのCookie送信に制限し、HttpOnlyはJavaScriptによる読み取りを防ぎ、SameSiteは一部のサイト間送信を制御します。それぞれ異なるリスクに対処し、ID更新の代わりにはなりません。
__Host-プレフィックス
対応ブラウザーでは、HTTPS経由で設定し、Secure、Path=/を指定し、Domain属性を付けないことが求められます。兄弟サブドメインが親ドメイン向けの同名Cookieを設定することを防ぐ助けになります。

攻撃の流れを段階ごとに解説

Step 1

攻撃者がセッション識別子を得る、または選ぶ

別の脆弱性や機能によって、攻撃者がIDを取得し、被害者のブラウザーに送信させます。経路はアプリによって異なります。URL内のセッションIDを受け付けない場合、通常のフィッシングリンクだけでは成立しません。

Step 2

被害者が攻撃者の知る識別子を使う

ブラウザーはそのIDをアプリに送り、アプリは有効な匿名セッションとして扱います。攻撃者は同じIDを識別または再利用できる必要があります。

Step 3

認証によってセッション権限が上がる

被害者がログインします。アプリがIDを更新せず既存IDに認証済みの本人情報を結び付けると、既知の識別子が認証済みセッションを指すようになります。

Step 4

攻撃者が変わっていないIDを再利用する

攻撃者が引き続きIDを送信でき、サーバーが無効化していなければ、被害者のセッションにアクセスできる可能性があります。権限変更時のID更新でこの連鎖を断てます。

仕組みと根本原因

セッション固定には、攻撃者が被害者の使う識別子に影響を与えられることと、ログイン成功時にアプリがそれを置き換えないことの両方が必要です。Cookie属性は多層防御になりますが、ID更新の代わりにはなりません。HttpOnlyはスクリプトからの読み取りを制限し、SecureはHTTPSを要求し、SameSiteは一部のサイト間送信を減らし、__Host-は対応ブラウザーでCookieの適用範囲を制限します。

login_view.py(脆弱な例)脆弱
# 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/")
login_view.py(Django安全例)安全
# 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

攻撃シーケンスと概念例

エンジニア向け防御・予防ルール

参考資料と関連するセキュリティトピック:セキュリティディレクトリを見る →
CWE-384:セッション固定 → OWASP:セッション管理チートシート → OWASP WSTG:セッション固定のテスト → Django:セッションとcycle_key() → MDN:HTTP Cookieとプレフィックス →