Dev Study
← Webセキュリティの実務
レッスン 9 / 12·約 9 分·第19期 Webセキュリティの実務

セッションIDは参照にすぎない — Cookie を消しても表の行は残る

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力今ここ
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回までは、ページの中で動くコードを見ました。今回は識別子の寿命を、ブラウザ側の Cookie とサーバ側の表に分けて見ます。

まず結論

セッションIDはサーバの表の1行を指す参照です。Cookie を消させても、表の行が残っていれば、同じ値を持つ要求はまだ通ります(自前サーバと curl をブラウザ役にした実測)。

ログイン時に ID を差し替えることと、ログアウト時に行を消すことは、状態が変わる瞬間に古い参照の行き先を残さないという同じ操作です。

ブラウザには、要求がどこから来たかは分かっても、その ID がまだ有効かは分かりません。ログアウトしたいという本人の意思は、サーバが自分の表から行を消したときに初めて効きます。

? 読む前に予測

ログアウト処理が `Set-Cookie: sid=; Max-Age=0` を返すだけで、サーバの表には手を付けないとします。ログアウト前に控えておいた sid の値を、ログアウト後に Cookie ヘッダで送り直すと、ログインが必要なページは何を返すでしょうか。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

値はブラウザに、意味はサーバにある

コース『アプリケーションの土台』の『状態を持たないHTTPとCookie/セッション』は、「cookie の値は表を引く鍵で、中身はプロセスのメモリにあります。ログアウトは1行消せば即座に効きます。」と書いていました。今回は、その1行を消さなかったらどうなるかを見ます。

OWASP Session Management Cheat Sheet も、値と意味の置き場を分けています。

The session ID must simply be an identifier on the client side, and its value must never include sensitive information or Personally Identifiable Information (PII).

The meaning and business or application logic associated with the session ID must be stored on the server side, and specifically, in session objects or in a session management database or repository.

ID は識別子にすぎず、「誰としてログインしているか」はサーバ側にしかありません。サーバに届くのは値だけで、その値をどこに控えていた誰が送ったのかは分かりません。すると、ログアウトで何を消すかが問題になります。

ID は参照、実体は表の行
ブラウザの Cookiesid=7818…(値だけ)
毎回の要求に値が乗る
サーバの表7818… → alice
ログアウトで消すのはどちらか
Cookie だけ消させる表の行は残る
表の行も消すどの控えも指す先を失う
  • ・サーバは届いた値で表を引くだけで、値がどこから来たかは区別しない
  • ・Cookie を消させても、同じ値の控えが別の場所にあれば表の行はまだ引ける
具体実物

Cookie だけ消すサーバと、表の行も消すサーバ

127.0.0.1 に最小の node サーバを立てました(macOS 26.5.2 / Node v24.21.0 / curl 8.7.1)。表はプロセス内の Map です。ログインでは前の行を消して新しい ID を発行し、ログアウトは Set-Cookie: sid=; Max-Age=0 を返します。違いはログアウトで表の行も消すかどうかだけです。

ログアウト処理(l9-server.mjs より)設定ファイル
if (req.url === '/logout' && req.method === 'POST') {
if (MODE === 'destroy' && sid) table.delete(sid);
return send(200, 'logged out (mode=' + MODE + ')', { 'Set-Cookie': 'sid=; Max-Age=0; Path=/' });
}

curl の cookie jar をブラウザ役にし、ログイン後の sid を別に控えておきます。ログアウトしてから、ブラウザ役の要求と、控えた値の送り直しを並べます。

ログアウト後(run 1。ステータス行と本文を grep で抜粋。----- の区切り行は教材側で追記)コマンド出力
----- mode=cookie-only
(cookie jar sid lines after logout: 0)
$ curl -si -b jar /me # browser side: cookie gone
HTTP/1.1 401 Unauthorized
no valid session
$ curl -si -H 'Cookie: sid=<copied sid>' /me # resend the copied ID
HTTP/1.1 200 OK
you are alice
----- mode=destroy
(cookie jar sid lines after logout: 0)
$ curl -si -b jar /me # browser side: cookie gone
HTTP/1.1 401 Unauthorized
no valid session
$ curl -si -H 'Cookie: sid=<copied sid>' /me # resend the copied ID
HTTP/1.1 401 Unauthorized
no valid session

どちらも cookie jar から sid は消え、ブラウザ役は 401 です。ここまでは区別がつきません。 控えた値を送ると、Cookie だけ消すサーバは you are alice を返しました。run 2 も同じ結果です。

「ログアウトしたらログイン画面に戻った」は、ブラウザが Cookie を送らなくなったことしか示しません。控えがどこにあるかは、サーバからは分かりません。実ブラウザでは試しておらず、curl の cookie jar で確かめた範囲の結果です。

✓ ここまでの確認

実演では、Cookie だけ消すサーバと表の行も消すサーバの両方で、ログアウト後にブラウザ役が /me を開くと 401 でした。ここから言えることはどれですか。

抽象原理

ログインの差し替えも、同じ操作

同じ Cheat Sheet は、権限が変わったら ID を作り直すよう求めています。

The session ID must be renewed or regenerated by the web application after any privilege level change within the associated user session.

節名は Renew the Session ID After Any Privilege Level Change で、典型例は認証の瞬間です。段落は the old or previous session ID must be destroyed で終わり、同じ節の後の段落(間にフレームワークの API を挙げる段落を挟む)が理由を The session ID regeneration is mandatory to prevent session fixation attacks と書きます。

無効化については、期限切れの節と Logout Button の節がこう書きます。

When a session expires, the web application must take active actions to invalidate the session on both sides, client and server. The latter is the most relevant and mandatory from a security perspective.

As described in Session_Expiration section, the web application must invalidate the session at least on server side.

並べると、ログイン時もログアウト時も、消す対象は古い値が指すサーバ側の行です。状態が変わる瞬間に古い参照の行き先を残さない、という1つの操作として読めます(この整理は教材側のものです)。なお OWASP は両側でと書いた直後にサーバ側を最重要としており、クライアント側が不要とは言っていません。

具体実物

unlink も、消すのは名前だけ

コース『OSとカーネルの土台』の『ファイルは名前ではない — 消しても消えないものがある』は、同じ形をファイルで見ていました。

unlink() deletes a name from the filesystem. If that name was the last link to a file and no processes have the file open, the file is deleted and the space it was using is made available for reuse.

If the name was the last link to a file but any processes still have the file open, the file will remain in existence until the last file descriptor referring to it is closed.

名前を消しても、開いたままの fd という別の参照があれば実体は残ります。Cookie を消しても、控えた値という別の参照があれば表の行は引けます。

ただし完全には重なりません。 カーネルは参照を把握しているので、最後の参照が閉じれば実体は回収されます。サーバは、発行した値を誰がどこに控えたかを知りません。行は自分で消すか、期限で消えるようにしておくしかありません。

✓ ここまでの確認

ログイン時に ID を差し替えることと、ログアウト時に表の行を消すこと。本文の整理で両者に共通するのはどれですか。

⇄ 2つを見比べる
具体例 A

Cookie だけ消させるログアウト

ブラウザ役の cookie jar から sid は消えましたが、サーバの表の行は残りました。控えた値を送ると 200 と you are alice が返りました。

mode=cookie-only、ログアウト後の送り直し(抜粋)コマンド出力
(cookie jar sid lines after logout: 0)
$ curl -si -H 'Cookie: sid=<copied sid>' /me # resend the copied ID
HTTP/1.1 200 OK
you are alice
具体例 B

fd を開いたままの unlink

名前は消えますが、開いている fd があればファイルは存在し続けます。領域が戻るのは最後の fd が閉じられたときです。

unlink(2) の条件表
最後の名前か 開いている fd 結果
はい 無い 削除され領域が戻る
はい 有る 最後の fd が閉じるまで存在する

ブラウザの Cookie と、ファイルの名前。消したのに残るものは、それぞれ何でしょうか。

消したものと、実体を指すもう1本の参照を、両方で探してください。

この章のまとめ

  • セッションIDはサーバの表を引く参照。値はブラウザ、意味はサーバにある(OWASP)。
  • Cookie だけ消させるサーバでも、ブラウザ側からは 401 になる。控えた値を送ると 200 が返った。
  • ログイン時の差し替えもログアウト時の破棄も、古い値が指すサーバ側の行を残さない操作。
  • OWASP はサーバ側の無効化を最重要かつ必須とするが、クライアント側を不要とは言っていない。
  • 結果が変わらない実験は証拠にならない。ログイン前 ID の 401 は、この実装では固定対策を示さない。
  • unlink と同じく消えるのは名前だけ。ただしサーバは控えの行方を知らないので、行は自分で消すか期限で消す。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

OWASP Session Management Cheat Sheet は、セッションが切れたときの無効化について、クライアント側とサーバ側をどう位置づけていますか。

用語集(3語)
セッションID
サーバがセッションの表を引くための識別子。OWASP は、値は識別子にすぎず、意味や業務上の情報はサーバ側のセッションオブジェクトやデータベースに置くよう求めている。
セッション固定
攻撃者が知っている ID を利用者に使わせたまま、その利用者にログインさせてしまう攻撃。OWASP は、ログイン時の ID の再生成を防止に必須としている。
unlink(2)
ファイルシステムから名前を1つ消すシステムコール。実体が消えるのは、最後の名前で、かつどのプロセスも開いていないときに限られる。