ログインしたのにセッション ID がそのままなら
一言でいうと
サーバーセッションは状態をサーバーが握り、ブラウザーには鍵(セッションID)だけを渡す仕組みです。そのためログアウト1回で即座に無効化できますが、その鍵がログインの前後で変わらないと、他人があらかじめ仕込んでおいた鍵で自分のログインに便乗されてしまいます。これがセッション固定(session fixation)です。
なぜ必要なのか
ログインフォームを作ると、普通はこう組みます。訪問されたらセッションを1つ作ってCookieで返し、ログインに成功したらそのセッションにusernameを入れます。問題はログインの前と後で同じセッションIDであることです。
攻撃者は、自分が知っているセッションIDを被害者のブラウザーにあらかじめ仕込めます(昔のサーバーがURLの;jsessionid=を受け付けていたことや、サブドメインからCookieを上書きすること)。被害者がそのIDのままログインすると、サーバーは「すでにあったそのセッション」にログイン状態だけを載せます。攻撃者は最初から知っていたそのIDで、ログイン済みのセッションをそのまま使えます。パスワードを知らなくても、他人のログインセッションを手に入れてしまうわけです。
防ぐ方法は、OWASP Session Management Cheat Sheetが次の1文で示しています。「The session ID must be renewed or regenerated by the web application after any privilege level change.」匿名から認証へ移る瞬間が、まさにその権限の変化です。ログインに成功したら新しいIDを発行し、古いIDは捨てます。すると、攻撃者が仕込んだIDはログイン直後に死んだ鍵になります。
どう動くのか
セッションストアをRedisのような外部ストアに置く理由は2つあります。1つ目は、Webサーバーが複数台あるとき、どのサーバーにリクエストが届いても同じセッションが見えることです(ローカルメモリに置くと、接続していたサーバーにしかセッションがありません)。2つ目は、サーバーが状態を握っているのでいつでも削除して即座に無効化できることです。これがサーバーセッションのJWTに対する決定的な利点です。
セッションIDは推測不能でなければなりません。OWASPは「at least 64 bits of entropy」を求めています。Pythonならsecrets.token_urlsafe(32)が256ビットを与えます。ストアのキーはsess:<id>で、値には最低限、ユーザー、作成時刻、最終アクティビティ時刻を入れます。
有効期限は2種類を併用します。OWASPの区別のとおりです。
idle timeout 마지막 활동 이후 N분간 조용하면 만료. 요청마다 갱신(sliding).
자리를 비운 세션이 영원히 살아 있지 않게 한다.
absolute timeout 세션을 만든 지 M시간이 지나면, 계속 쓰고 있었더라도 만료.
탈취된 세션이 무한정 연장되지 않게 하는 상한이다.
Redisなら、idleはキーのTTLで自然に表せます。リクエストごとにEXPIREでTTLを延ばし直せばslidingになります。しかしTTLだけではabsoluteを表せません(リクエストごとに延びるので永遠に死にません)。そのため値の中に作成時刻を別に入れ、リクエストのたびにnow - created > absoluteならidleのTTLが残っていても拒否します。
Cookieの属性は次のモジュールで詳しく見ますが、セッションCookieの最低ラインは、HttpOnly(スクリプトに読ませない)、Secure(HTTPSでのみ)、SameSite=Lax以上、そして名前に__Host-接頭辞を付けてサブドメインに上書きさせないことです。
現場での姿
ログインはできるのに「たまに別の人のアカウントでログインされる」という報告が、まれに入ります。たいていはセッションIDを再発行しないコードで、共有PCやサブドメインのXSSを通じて他人のセッションに便乗された場合です。ログを見ても正常なログインなので原因が見えません。セッションIDがログインの前後で同じだという事実は、アプリケーションのコードを見て初めてわかります。
逆に、サーバーセッションの利点が光る場面もあります。アカウントが侵害されたという報告が来たら、そのユーザーのセッションキーをストアから消すだけで、すべての端末から即座にログアウトさせられます。JWTだったら、有効期限まで待つか、拒否リストを別に運用する必要があります(後のモジュールで扱います)。
その代わり、セッションストアはログイン全体の単一障害点になります。Redisが止まると、全ユーザーが一斉にログアウトされたのと同じです。そのため運用ではレプリケーションとフェイルオーバーを用意し、ストアに届かないときに「とりあえずログイン済みとみなす」側に倒れないように(fail-closed)組みます。キーごとにTTLを必ず付けるのも同じ理由です。TTLのないセッションキーは、ログアウトせずに去ったユーザーの数だけ溜まってメモリを食い、いつかエビクションポリシーが生きているセッションまで消し始めます。
次のラボですること
Redisをセッションストアとして接続したログインサーバーを立て、Cookieの属性を点検し、ログインの瞬間にセッションIDが再発行されるかを実際のリクエストで確認します。あらかじめ仕込んだセッションIDでログインしてみて、そのIDがログイン直後に無効になるところまで見ます。最後に、絶対期限を過ぎたセッションがidle TTLと無関係に拒否されるかを確認します。