TT Lab
はじめる
学ぶ 学習パス コース

セッションとトークン — ブラウザからメッシュまで

__Host- セッションクッキーを発行し、正しく消す

TT Labで続きを見る

目標

127.0.0.1:8301にログインサーバーを立てて__Host-sidセッションCookieを発行・検査・取り消しし、Cookieがブラウザー(curl)のストアに実際に受け入れられ、削除されるところまで確認します。

なぜ重要なのか

Cookieの問題はサーバーのログでは見えません。サーバーはSet-Cookieを正常に出したのに、ブラウザーが接頭辞のルールやSecureの条件に引っかかって黙って捨てると、症状は「たまにログインできない」としか現れません。そこでこのラボは、レスポンスヘッダーと合わせてcurlのCookieストアを見ます。curl 8.5も__Host-・__Secure-のルールに反するCookieは保存しないので、ストアに残っているかどうかが、受け入れられたかどうかの近似になります。ログアウトも同じ目で見ます。Cookieを消すのはブラウザーへのお願いにすぎず、値を書き写した人にとって意味があるのは、サーバーの記録が消えたかどうかだけです。

ステップ

  1. /root/st/cookie/app.pyを127.0.0.1:8301で起動してください。GET /healthzは200と{"ok":true}を返します。
  2. POST /loginにJSON{"user":"alice","password":"wonderland"}を送ると、200と{"user":"alice"}を返し、__Host-sidCookieを発行します。値はsecretsで作った128ビット以上のランダム値(base64urlで22文字以上)で、ログインのたびに変わる必要があり、ユーザー名を含めてはいけません。bob/builderでもログインできます。
  3. セッションCookieにSecure、HttpOnly、SameSite=Lax、Path=/を付け、DomainとMax-Age/Expiresは付けないでください(ホスト専用のセッションCookie)。aliceでログインしながらcurl -c /root/st/cookie/jar.txtでCookieストアを残してください。そのファイルの__Host-sidの行は#HttpOnly_127.0.0.1で始まり、パスが/、SecureがTRUE、有効期限が0である必要があります。
  4. GET /meは、有効な__Host-sidがあれば200と{"user":"<사용자>"}(プレースホルダーはユーザー名です)を返し、Cookieがない場合やサーバーが発行したことのない値の場合は401を返します。
  5. 間違ったパスワード(alice/wrongpass)と存在しないユーザー(mallory)のPOST /loginは401を返し、Set-Cookieが1行もあってはいけません。
  6. POST /logoutは200を返し、サーバー側のセッションを消します。レスポンスには__Host-sidを消すSet-Cookie(Max-Age=0)が必要で、curlのストアがその削除を実際に受け入れてCookieが消える必要があります。ログアウト前にコピーしておいたCookieの値でGET /meを呼ぶと401になる必要があります。ステップ3のjar.txtは上書きせず、別のストアファイルで試してください。
  7. https://app.example.com/loginのレスポンスとして次のSet-Cookie値が届いたとき、ブラウザーがCookieを保存するかどうかを判定し、/root/st/cookie/prefix.txtに번호=acceptまたは번호=rejectの形で(プレースホルダーは番号です)1行ずつ書いてください。
    • 1 __Host-sid=a1; Secure; HttpOnly; Path=/; SameSite=Lax
    • 2 __Host-sid=a2; HttpOnly; Path=/; SameSite=Lax
    • 3 __Host-sid=a3; Secure; Path=/; Domain=app.example.com
    • 4 __Host-sid=a4; Secure; Path=/account
    • 5 __Secure-theme=dark; Secure; Path=/account; Domain=example.com
    • 6 __Secure-theme=dark; Path=/
    • 7 __host-sid=a7; Path=/
    • 8 theme=dark; Path=/; Domain=example.com
  8. /root/st/cookie/e2e.shで、ログイン、Cookieでの/me、Cookieなしの/me、間違ったパスワード、ログアウト、古いCookieの再利用を順に試してください。ログアウト後にストアに残った__Host-sidの行数を数え、/root/st/cookie/e2e.outにlogin=200 me=200 no_cookie=401 bad_password=401 logout=200 replay=401 jar_after_logout=0を1行で書いてください。

参考

ログインサーバーを起動する

/root/st/cookie/app.pyを127.0.0.1:8301で起動してください。GET /healthzは200と{"ok":true}を返します。

標準ライブラリのhttp.serverで十分です。サーバーはバックグラウンドで起動し、/healthzが応答するまで待ってから次へ進んでください。

ログインでセッションCookieを発行する

POST /loginにJSON{"user":"alice","password":"wonderland"}を送ると、200と{"user":"alice"}を返し、__Host-sidCookieを発行します。値はsecretsで作った128ビット以上のランダム値(base64urlで22文字以上)で、ログインのたびに変わる必要があり、ユーザー名を含めてはいけません。bob/builderでもログインできます。

セッション値は意味を持たない鍵です。randomモジュールは予測できるので、暗号論的乱数を使うsecretsモジュールで、URLに安全な文字列を作る関数を探してみてください。誰のセッションかはサーバーがdictに記録します。

Cookieの属性とストアを確認する

セッションCookieにSecure、HttpOnly、SameSite=Lax、Path=/を付け、DomainとMax-Age/Expiresは付けないでください(ホスト専用のセッションCookie)。aliceでログインしながらcurl -c /root/st/cookie/jar.txtでCookieストアを残してください。そのファイルの__Host-sidの行は#HttpOnly_127.0.0.1で始まり、パスが/、SecureがTRUE、有効期限が0である必要があります。

Domainは書くほど範囲が広がります。省略して初めて、送ったホストだけに送られます。有効期限を書かなければ、ブラウザーを閉じると消えるセッションCookieになります。curlのストアファイルはタブ区切りの7列で、HttpOnlyのCookieは最初の列の前に印が付きます。

Cookieでユーザーを見分ける

GET /meは、有効な__Host-sidがあれば200と{"user":"<사용자>"}(プレースホルダーはユーザー名です)を返し、Cookieがない場合やサーバーが発行したことのない値の場合は401を返します。

Cookieが付いて届いたという事実だけでは何も証明されません。その値でサーバーのセッション記録を探し、ユーザーを取り出す必要があります。

失敗したログインにはCookieを渡さない

間違ったパスワード(alice/wrongpass)と存在しないユーザー(mallory)のPOST /loginは401を返し、Set-Cookieが1行もあってはいけません。

Cookieを作るコードがパスワード検査より前にあると、失敗してもCookieが出てしまいます。パスワードの比較には、時間差が漏れない関数を使うほうがよいです。

ログアウトを本物にする

POST /logoutは200を返し、サーバー側のセッションを消します。レスポンスには__Host-sidを消すSet-Cookie(Max-Age=0)が必要で、curlのストアがその削除を実際に受け入れてCookieが消える必要があります。ログアウト前にコピーしておいたCookieの値でGET /meを呼ぶと401になる必要があります。ステップ3のjar.txtは上書きせず、別のストアファイルで試してください。

Cookieの削除はお願いにすぎません。値を書き写した人にとっては、サーバーの記録が消えたかどうかだけが意味を持ちます。そして削除用Cookieもまた1つのCookieなので、__Host-接頭辞の条件をもう一度通過して初めてブラウザーが受け入れます。

接頭辞のルールを判定する

https://app.example.com/loginのレスポンスとして次のSet-Cookie値が届いたとき、ブラウザーがCookieを保存するかどうかを判定し、/root/st/cookie/prefix.txtに번호=acceptまたは번호=rejectの形で(プレースホルダーは番号です)1行ずつ書いてください。1 __Host-sid=a1; Secure; HttpOnly; Path=/; SameSite=Lax・2 __Host-sid=a2; HttpOnly; Path=/; SameSite=Lax・3 __Host-sid=a3; Secure; Path=/; Domain=app.example.com・4 __Host-sid=a4; Secure; Path=/account・5 __Secure-theme=dark; Secure; Path=/account; Domain=example.com・6 __Secure-theme=dark; Path=/・7 __host-sid=a7; Path=/・8 theme=dark; Path=/; Domain=example.com

接頭辞ごとに要求される条件の一覧を先に書き出し、1行ずつ照合してください。__Host-は3つ、__Secure-は1つです。ブラウザーが接頭辞を大文字と小文字を区別して照合するかどうかも、規格で確認してみてください。#で始まる行はコメントとして残せます。

全体の流れを一度に証明する

/root/st/cookie/e2e.shで、ログイン、Cookieでの/me、Cookieなしの/me、間違ったパスワード、ログアウト、古いCookieの再利用を順に試してください。ログアウト後にストアに残った__Host-sidの行数を数え、/root/st/cookie/e2e.outにlogin=200 me=200 no_cookie=401 bad_password=401 logout=200 replay=401 jar_after_logout=0を1行で書いてください。

前のステップでの確認を、1つのCookieストアでつなげて実行すれば足ります。再利用の試験に使う古い値は、ログアウトする前にストアから取り出しておく必要があります。