[BEE-1004] 會話管理
INFO
Session(會話)是伺服器端與客戶端識別碼綁定的狀態。安全地生成 session、在傳輸中保護 session ID,以及可靠地銷毀 session,是三個不可妥協的要求。
背景
HTTP 是無狀態協定。使用者完成身份驗證後,伺服器需要一種機制在後續請求中識別同一位使用者,而不需要每次都提供憑證。傳統解法是伺服器端 session:伺服器建立一筆以隨機識別碼為鍵的記錄,儲存後將識別碼以 cookie 形式傳送給客戶端。每個後續請求都帶上該 cookie,伺服器查找記錄後即可知道是誰在發出請求。
Session 看似簡單,卻包含數個反覆出現於漏洞報告的失敗模式:
- 可預測的 session ID 讓攻擊者能夠猜測有效的 session。
- Session fixation(會話固定)攻擊讓攻擊者能在受害者登入前預設 session ID。
- 缺少 cookie 安全屬性會將 session ID 暴露給 JavaScript 或明文 HTTP 截取。
- 缺少伺服器端的 session 銷毀,意味著「登出」只是表面動作——session 依然可用。
OWASP Session Management Cheat Sheet(2024)與 NIST SP 800-63B(第 7 節,重新驗證)為本文要求提供了規範性依據。
原則
1. Session ID 必須使用密碼學安全的來源生成
Session ID 的不可預測性取決於其隨機性來源。可預測的 ID——序列計數器、時間戳記、使用者 ID——都能被攻擊者列舉。
要求:
- 使用 CSPRNG(Cryptographically Secure Pseudorandom Number Generator,密碼學安全偽隨機數生成器)生成 session ID,絕不使用
Math.random()或等效方式。 - Session ID 必須攜帶至少 128 bits 的熵(OWASP 建議;NIST SP 800-63B 對隨機值的最低要求為 64 bits,128 bits 是目前的實務標準)。
- Session ID 不得編碼任何可識別使用者的資料。識別碼必須是不透明的。
2. Cookie 屬性必須正確設定
Session cookie 是 session ID 竊取的主要攻擊面。以下四個屬性在正式環境中全部必須設定。
| 屬性 | 要求 | 用途 |
|---|---|---|
HttpOnly | 必須 | 防止 JavaScript(document.cookie)讀取 session ID。降低 XSS 竊取 session 的風險。 |
Secure | 必須 | 將 cookie 限制於 HTTPS 連線。防止明文傳輸。(RFC 6265,第 4.1.2.5 節) |
SameSite=Strict 或 Lax | 應該 | 控制跨站 cookie 提交。Strict 封鎖所有跨站傳送;Lax 允許頂層導航。降低 CSRF 風險。 |
Path=/ | 應該 | 將 cookie 限制於應用程式路徑。避免過於寬泛的 Domain 範圍。 |
包含所有必要屬性的 Set-Cookie 標頭範例:
Set-Cookie: sessionId=a3f8c2d1e9b047f6a2d8c4e1f9b3a7d5;
HttpOnly;
Secure;
SameSite=Strict;
Path=/不要在 session cookie 上設定 Expires 或 Max-Age。沒有這些屬性的 session cookie 在瀏覽器 session 結束時即過期,縮短了被竊 cookie 可被重放的時間窗口。
3. 驗證後必須重新生成 Session ID(防止 session fixation)
Session fixation 是一種攻擊,攻擊者在受害者驗證前建立已知的 session ID,等受害者登入後接管其 session。防禦措施是無條件的:成功登入後立即丟棄驗證前的 session ID,發放新的 ID。
任何權限提升後也應套用相同的重新生成機制(例如,使用者確認密碼以存取敏感操作)。
4. 登出與逾時時必須在伺服器端銷毀 Session
僅刪除 cookie 的客戶端登出是不夠的。伺服器必須在其 session store 中刪除或標記該 session 記錄為無效。登出後重放的被竊 cookie 必須被拒絕。
應獨立執行兩種逾時策略:
- 閒置逾時(Idle timeout):在一段閒置期後銷毀 session(標準應用程式建議 15–30 分鐘;高權限操作建議 2–5 分鐘)。NIST SP 800-63B 要求在 AAL2 等級下閒置 30 分鐘後重新驗證。
- 絕對逾時(Absolute timeout):不論活動狀況,在固定時鐘時間後銷毀 session(標準應用程式建議 8–24 小時;敏感情境建議 4–8 小時)。NIST SP 800-63B 要求在 AAL2 等級下至少每 12 小時重新驗證一次。
兩種逾時都必須在伺服器端執行。客戶端倒數計時僅為參考 UI。
5. Session 儲存必須支援可靠的查找與刪除
概念層面的三種常見方式:
- 進程內記憶體(In-process memory):速度快,無基礎設施成本;伺服器重啟後所有 session 消失;無法跨多個實例擴展。
- 資料庫儲存(Database-backed):session 在重啟後持續存在,可跨實例運作;需要在 session ID 欄位建立索引;透過 TTL 或清理任務支援過期。
- 分散式快取(Distributed cache):讀取速度快,內建 TTL 過期,可水平擴展;引入外部依賴;需要快取層的高可用性。
選擇取決於擴展性要求。多實例部署需要共享儲存(資料庫或快取),或搭配負載均衡器的 sticky session。沒有共享儲存的 sticky session 會使跨叢集的伺服器端銷毀變得複雜。
6. Session 與 token:選擇合適的工具
Session 與 token(JWT)不可互換。主要的取捨在於有狀態性與擴展性:
| 面向 | 伺服器端 session | 無狀態 token(JWT) |
|---|---|---|
| 撤銷 | 即時——刪除 session 記錄 | 困難——若無封鎖清單,token 在過期前持續有效 |
| 伺服器狀態 | 必要(session store) | 無(透過簽名驗證) |
| 擴展性 | 大規模需要共享 session store | 每個節點本地驗證 |
| Payload 大小 | 極小的 cookie(僅 ID) | Token 攜帶所有 claims |
| 可稽核性 | 容易——伺服器日誌包含 session 事件 | 需要彙整 token 呈現的日誌 |
當即時撤銷是必要條件時(例如金融應用程式、管理控制台),使用伺服器端 session。當水平擴展且無共享狀態是優先考量,且短暫的過期時間窗口(通常 15 分鐘)是可接受的撤銷延遲時,使用無狀態 token。Token 設計請參見 BEE-1002。
視覺化
以下圖表展示完整的 session 生命週期,從登入到登出與逾時。
範例
以下虛擬碼展示登入時建立 session 與每次後續請求的驗證流程。刻意採用框架無關的寫法。
# --- Login handler ---
function handle_login(request):
credentials = parse_credentials(request)
user = authenticate(credentials)
if user is null:
return response(401, "Invalid credentials")
# Discard any pre-login session (session fixation prevention)
old_session_id = read_cookie(request, "sessionId")
if old_session_id is not null:
session_store.delete(old_session_id)
# Create new session with fresh cryptographic ID
session_id = csprng.generate_hex(32) # 256 bits
session_data = {
user_id: user.id,
created_at: now(),
last_active: now(),
}
session_store.set(session_id, session_data, ttl=absolute_timeout)
response = response(200, "Logged in")
response.set_cookie(
name = "sessionId",
value = session_id,
http_only = true,
secure = true,
same_site = "Strict",
path = "/",
# No Expires / Max-Age — session cookie
)
return response
# --- Request validation middleware ---
function require_session(request):
session_id = read_cookie(request, "sessionId")
if session_id is null:
return response(401, "No session")
session = session_store.get(session_id)
if session is null:
return response(401, "Session not found or expired")
idle_seconds = now() - session.last_active
if idle_seconds > idle_timeout:
session_store.delete(session_id)
return response(401, "Session expired (idle timeout)")
# Refresh last_active
session.last_active = now()
session_store.set(session_id, session, ttl=absolute_timeout)
request.user_id = session.user_id
return next(request)
# --- Logout handler ---
function handle_logout(request):
session_id = read_cookie(request, "sessionId")
if session_id is not null:
session_store.delete(session_id) # Server-side invalidation
response = response(200, "Logged out")
response.set_cookie(
name = "sessionId",
value = "",
max_age = 0, # Instruct browser to delete cookie
)
return response常見錯誤
1. 使用可預測的 session ID。
序列整數、使用者 ID 和時間戳記都不是 session ID。任何攻擊者能計算或列舉的值都是可列舉的。只使用 CSPRNG 輸出。
2. 登入後未重新生成 session ID。
若伺服器在驗證邊界前後重用相同的 session ID,攻擊者在登入前植入已知 session ID 後,即可在受害者登入後接管其 session。每次成功驗證後必須無條件重新生成。
3. 缺少 HttpOnly 或 Secure 旗標。
沒有 HttpOnly 的 session cookie 可被注入的 JavaScript 竊取。沒有 Secure 的 cookie 可在 HTTP 明文頁面或攻擊者控制的網路上被截取。兩個旗標在正式環境中都是必要的;沒有任何正當理由可以缺少其中一個。
4. 沒有 session 逾時。
沒有閒置或絕對逾時的 session 永遠有效,或直到使用者主動登出。如果使用者從未登出(關閉分頁、裝置被竊),session 將無限期地可被利用。實作伺服器端執行的閒置和絕對逾時。
5. 登出時未在伺服器端銷毀 session。
清除 session cookie 而不刪除伺服器端記錄,會讓 session 繼續可用。任何在登出前截取到 session ID 的攻擊者都可以繼續使用它。登出必須先刪除 session store 中的 session 記錄,再清除 cookie。
相關 BEE
- BEE-1001: Authentication vs Authorization — 身份驗證管道背景
- BEE-1002: Token-Based Authentication — session 的無狀態替代方案
- BEE-2001: Transport Security — Secure cookie 底層的 HTTPS 要求
- BEE-2004: Secrets Management — session store 中的 secrets 處理
參考資料
- OWASP, "Session Management Cheat Sheet" (2024). https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html
- Barth, A., "HTTP State Management Mechanism" RFC 6265 (April 2011). https://datatracker.ietf.org/doc/html/rfc6265
- NIST, "Digital Identity Guidelines: Authentication and Lifecycle Management" SP 800-63B (2017, updated 2020). https://pages.nist.gov/800-63-3/sp800-63b.html
- OWASP, "Top 10 — A07:2021 Identification and Authentication Failures" (2021). https://owasp.org/Top10/A07_2021-Identification_and_Authentication_Failures/