Learning Objectives
- 說明工作階段 ID 與簽章 Cookie 兩種設計的差異
- 解釋 Flask 的 session 為何可以解碼卻無法竄改,以及金鑰外洩的後果
- 在模擬網站上觀察工作階段劫持與工作階段固定,並修補登入與登出流程
- 依需求設定 Cookie 的 HttpOnly、Secure、SameSite 與有效期,並說明各屬性的限制
HTTP 的無狀態特性與工作階段
學生登入選課系統後,接著開啟課表、送出加選,每一步都是獨立的 HTTP 請求。HTTP 是無狀態協定(stateless),每個請求的語意可以單獨理解,協定本身不記得上一個請求來自誰。網站若要知道「這個請求來自剛才登入的那位學生」,必須讓瀏覽器在每次請求都帶上一項憑據,這項憑據通常放在 Cookie。
建立在這項憑據之上的連續互動稱為工作階段(session)。對伺服器而言,持有憑據的人會被視為已登入的使用者,所以這項憑據的價值等同於一次已通過驗證的登入。常見的設計有兩種:
| 設計 | 瀏覽器保存的內容 | 內容存放位置 | 伺服器端能否立即撤銷 |
|---|---|---|---|
| 工作階段 ID(session ID) | 一個隨機字串 | 伺服器(資料庫或快取) | 可以,刪除紀錄即可 |
| 簽章 Cookie(Flask 預設) | 資料本身+簽章 | 瀏覽器 | 不能直接撤銷,需要另外設計 |
工作階段 ID 必須無法猜測。OWASP 建議至少 64 位元的熵,並由密碼學安全的亂數產生器產生,例如 Python 的 secrets.token_urlsafe()。流水號或時間戳記都可以被推算,不應作為工作階段 ID。
簽章與加密的差別
Flask 預設的 session 把資料序列化後放進 Cookie,並以 secret_key 計算簽章。簽章的作用是偵測竄改,不是隱藏內容。Flask 文件的說明是:使用者可以看到 Cookie 的內容,但不能修改,除非知道用來簽章的金鑰。
這項設計有三個直接的後果:
- 不要把機密放進 session。 任何人都可以解碼 Cookie 的內容。
- 金鑰是整個機制的基礎。 持有金鑰的人可以為任意帳號簽出有效的 Cookie,等同於能以任何人的身分登入。金鑰不可以寫死在程式碼或提交到版本庫(第 14 課)。
- 更換金鑰會讓所有既有工作階段失效。 這是金鑰外洩後必要的處理,代價是所有使用者都需要重新登入。金鑰外洩時,不可以把舊金鑰放進 Flask 的
SECRET_KEY_FALLBACKS,否則偽造的 Cookie 依然有效。
工作階段劫持與登出
工作階段劫持(session hijacking)是指他人取得有效的憑據後,以該使用者的身分操作。伺服器看到的只是一個有效的 Cookie,無法從這一點分辨持有者。憑據外流的途徑包括未加密的傳輸(第 4 課)、跨站腳本(第 8 課)、寫進日誌(第 16 課),以及共用電腦上未登出的瀏覽器。
因此,登出必須讓憑據在伺服器端失效。只請瀏覽器刪除 Cookie,只影響這一台瀏覽器,已經流出的副本依然有效。OWASP 的工作階段管理指引明確要求在伺服器端使工作階段失效。
簽章 Cookie 在這一點上有結構性的限制。Flask 的 session.clear() 只讓伺服器回應一個清除 Cookie 的指令,伺服器沒有保存任何可以刪除的紀錄。舊 Cookie 的副本在時效內(PERMANENT_SESSION_LIFETIME,預設 31 天)仍能通過簽章驗證;Flask 文件提到,縮短這個值有助於降低重送攻擊的風險。如果需要立即撤銷的能力,可以改用伺服器端工作階段,或在資料庫保存每位使用者的工作階段版本號並放進 Cookie 比對,兩者都需要在每個請求查詢伺服器端的狀態。
工作階段固定與登入後換發
工作階段固定(session fixation)與劫持的方向相反。攻擊者不去竊取憑據,而是先讓受害者的瀏覽器使用一個攻擊者已知的 sid,等受害者登入。如果網站在登入時沿用原本的 sid,這個值就變成已登入的憑據,攻擊者不需要知道密碼。
防護方式是權限改變時換發新的識別碼。OWASP 的指引要求在任何權限層級改變之後重新產生工作階段 ID,登入是最重要的時間點,舊的識別碼也要一併銷毀。
Flask 預設的簽章 Cookie 沒有伺服器端的識別碼,所以沒有完全相同的問題,但有類似的風險:登入前寫入 session 的資料會延續到登入之後。範例在登入成功時都先呼叫 session.clear(),目的是讓登入後的 session 只包含登入時寫入的內容。
Cookie 的屬性
伺服器以 Set-Cookie 標頭發出 Cookie 時,可以附加屬性來限制 Cookie 的使用範圍。各屬性處理的問題不同,也都有限制:
| 屬性 | 作用 | 不能解決的問題 |
|---|---|---|
HttpOnly |
頁面的 JavaScript 讀不到這個 Cookie | 不能阻止跨站腳本本身;腳本仍可以用目前使用者的身分送出請求(第 8 課) |
Secure |
只在 HTTPS 請求送出(localhost 例外) | 不代表網站內容可信(第 4 課) |
SameSite |
限制跨站請求是否帶上 Cookie | 不能取代 CSRF 防護權杖(第 9 課) |
Domain |
指定可以收到 Cookie 的網域及其子網域 | 設定後範圍會變大;省略時只回傳給原主機,工作階段 Cookie 應該省略 |
Max-Age/Expires |
設定有效期,兩者並存時以 Max-Age 為準 |
只控制瀏覽器何時刪除;已流出的副本仍需由伺服器端的時效檢查處理 |
Flask 的工作階段 Cookie 可以用設定值控制:SESSION_COOKIE_HTTPONLY(預設 True)、SESSION_COOKIE_SECURE(預設 False)、SESSION_COOKIE_SAMESITE(預設不設定)。瀏覽器在未設定 SameSite 時的行為並不一致,部分瀏覽器以 Lax 為預設,所以應該明確設定。
Secure 的效果在本課的模擬環境中無法觀察:瀏覽器對 localhost 視為例外,模擬瀏覽器也不區分 HTTP 與 HTTPS。下方練習只確認標頭的內容。
重點整理
- HTTP 是無狀態協定,工作階段的憑據通常放在 Cookie,持有憑據的人會被視為已登入
- 工作階段 ID 把資料存在伺服器,可以立即撤銷;簽章 Cookie 把資料存在瀏覽器,不能直接撤銷
- 簽章能偵測竄改但不隱藏內容,機密資料不應放進 Flask 的 session;金鑰外洩時,任何帳號的 Cookie 都可以被偽造,必須更換金鑰
- 登出要讓憑據在伺服器端失效;只刪除瀏覽器的 Cookie,不影響已流出的副本
- 登入等權限改變的時間點要換發新的識別碼,並銷毀舊的識別碼,以防止工作階段固定
HttpOnly、Secure、SameSite各自處理不同的問題,都不能取代修補跨站腳本與 CSRF 防護權杖- 只在自己擁有或獲得明確授權的系統上進行測試
下一課討論注入類的漏洞:SQL 注入。
小測驗
關於 Flask 預設的 session,下列敘述何者正確?
發現 Flask 的 secret_key 被提交到公開版本庫,最優先的處理是什麼?
一個網站使用 Flask 預設的簽章 Cookie,登出時呼叫 session.clear()。Cookie 在登出前被他人複製,這份副本之後的狀況如何?
為什麼登入成功後要換發新的工作階段 ID?
將工作階段 Cookie 設為 HttpOnly 後,下列何者仍然可能發生?
工作階段 Cookie 設定了 Domain=campus.example,會造成什麼影響?