iT邦幫忙

2026 iThome 鐵人賽

DAY 20
1

💡 今日學習目標:把「使用者登入之後拿到的那張憑證」當成一條有五個關卡的生命線來看,逐關掌握 Session Cookie 的安全屬性、JWT 的簽章驗證陷阱,以及 2025 年最新的密碼政策該怎麼訂。


📌 前言:Day 17 存好了密碼,然後呢?

Day 17 我們花了整篇處理「密碼該怎麼存」。但你有沒有想過:使用者登入成功之後,那組密碼就再也用不到了

接下來一整個小時、一整天,伺服器認的不是密碼,而是登入當下發給他的那張憑證(Session ID 或 JWT Token)。

換句話說:你把密碼保護得再好,只要那張憑證能被偷走或偽造,前面所有的功夫都白費了。

這就是 A07:2025 認證失效 (Authentication Failures)。它在 2025 年名次沒有變動,仍然是第 7 名,只是名稱從「Identification and Authentication Failures」精簡了。但它的數字一點都不小:涵蓋 36 個 CWE、最高發生率 15.80%、累計出現 1,120,673 次,是十大類別中出現次數相當前段的一類


🔍 核心觀念:憑證的五個關卡

大部分文章講到這個主題,就是給你「Cookie 三件套」加上「JWT 要驗簽章」。那確實是對的,但那只涵蓋了五分之二。

登入憑證的五個關卡:發放、傳輸、儲存、驗證、失效,各有對應的攻擊與防守

請特別注意第 1 關與第 5 關,它們是最常被整篇跳過、卻最常出事的兩關。我們一關一關來。


🛠️ 第 1 關|發放:登入成功後,一定要換一組新的

這一關的攻擊叫 Session Fixation(會話固定),運作方式是反過來的:攻擊者不是偷你的 Session ID,而是先發一組他自己知道的給你

1. 攻擊者先造訪網站,拿到一組 Session ID:abc123
2. 他把 https://例子.com/?sessionid=abc123 這種連結傳給受害者
3. 受害者點進去、輸入帳密、登入成功
4. 如果伺服器「沿用」了 abc123 這組 ID,
   攻擊者手上那組 abc123 現在就是一組已登入的管理員身分了

防守方式非常簡單,一行就解決:

Function OnLoginSuccess(user):
    Session.Regenerate()          // ← 關鍵:丟掉舊 ID,產生全新的一組
    Session.Set("user_id", user.Id)

各語言的寫法:PHP 是 session_regenerate_id(true)、ASP.NET 要手動 abandon 後重建、Express 用 req.session.regenerate()

🔑 為什麼這一關最常被漏掉? 因為漏了它,功能完全正常。使用者登入得進去、什麼錯誤訊息都不會有,測試也會通過。它是那種「只有攻擊者會發現」的漏洞,而這正是 Day 01 講的惡意路徑(Evil Path):你只測了「使用者會怎麼用」,沒測「攻擊者會怎麼用」。


🛠️ 第 2、3 關|傳輸與儲存:Cookie 的安全屬性

❌ 不安全
Set-Cookie: session_id=abc123xyz; Path=/

✅ 安全
Set-Cookie: __Host-session=abc123xyz; Path=/; Secure; HttpOnly; SameSite=Lax
屬性 作用
HttpOnly JavaScript 無法透過 document.cookie 讀取。即使網站有 XSS,攻擊者也偷不走它
Secure 只在 HTTPS 連線傳送,明文連線一律不帶
SameSite=Lax 從別的網站發起的請求不會帶出這個 Cookie,大幅降低 CSRF
__Host- 前綴 瀏覽器層級的強制保險

那個 __Host- 前綴值得多講一句

這是很多人沒用過、但成本為零的一招。當 Cookie 名稱以 __Host- 開頭,瀏覽器會強制檢查:必須有 Secure、必須來自 HTTPS、不可以有 Domain 屬性Path 必須是 /。任何一項不符,瀏覽器直接拒收。

好處是:它讓你不可能不小心設錯。而且因為禁止 Domain 屬性,子網域就無法覆寫這個 Cookie,這擋掉了一整類「從一個被攻陷的子網域打主站」的攻擊。

關於 SameSite 的兩個實務細節

  • 沒寫 SameSite 不等於沒有防護。現代瀏覽器(Chrome 80 之後)會預設當成 Lax。但別依賴預設值,不同瀏覽器行為不一致,明確寫出來。
  • SameSite=None 一定要配 Secure,否則瀏覽器會直接丟棄這個 Cookie。這是很多人在做跨站嵌入功能時卡住的原因。

🛠️ 第 4 關|驗證:JWT 的三個經典陷阱

// ❌ 錯誤:只解碼,不驗簽
Function HandleAPIRequest(req):
    token = req.GetHeader("Authorization")
    payload = JWT.DecodePayloadWithoutVerification(token)   // 🚨
    Return ProcessRequest(payload.UserId)

這段為什麼致命?因為 JWT 的 Payload 只是 Base64 編碼,不是加密。任何人都可以把 Token 貼到 jwt.io 上看內容、把 "role": "user" 改成 "role": "admin"、重新編碼送出。沒驗簽章 = 讓使用者自己填寫自己的權限。

⚠️ 順帶一提:既然 Payload 是任何人都看得到的,就不要把敏感資料放進去。身分證號、內部系統路徑、其他使用者的 email,放進 JWT 等於直接公開。

陷阱一:"alg": "none"

JWT 規格允許 alg 設成 none,表示「這個 Token 沒有簽章」。早期有不少函式庫真的會照著做,攻擊者把 header 改成 {"alg":"none"}、把簽章欄位清空,就通關了。

陷阱二:演算法混淆(RS256 ➔ HS256)

這一個更精巧,也是實務上真正被利用的手法:

  • RS256 是非對稱的:用私鑰簽名、用公鑰驗章。公鑰是公開的,本來就不需要保密。
  • HS256 是對稱的:簽名與驗章用同一把金鑰

現在假設你的伺服器這樣寫:JWT.Verify(token, publicKey),把公鑰傳進一個通用的驗證函式。攻擊者做的事情是:

  1. 把 header 的 algRS256 改成 HS256
  2. 拿你那把公開的公鑰當作 HMAC 的密鑰,去簽自己偽造的 Payload

伺服器收到後,看到 alg: HS256,就把手上那把公鑰當成 HMAC 密鑰去驗,結果當然通過。

🔑 問題的根源是:伺服器相信了 Token 自己宣稱的演算法。而那個 header 是攻擊者可以隨意修改的。

正確寫法

// ✅ 安全:演算法白名單寫死在程式碼裡
Function HandleAPIRequest(req):
    header = req.GetHeader("Authorization")
    If NOT header.StartsWith("Bearer "):
        Return Error(401)
    token = header.Substring(7)

    // 關鍵:AllowedAlgs 是我們自己指定的,不從 token 的 header 讀
    claims = JWT.VerifyAndDecode(token, key,
                                 AllowedAlgs = ["RS256"],   // 只接受這一種
                                 ExpectedAudience = "my-api",
                                 ExpectedIssuer   = "https://auth.example.com")

    If claims.IsExpired():
        Return Error(401, "Token Expired")

    Return ProcessRequest(claims.UserId)

三個重點:演算法白名單寫死exp 過期時間audiss(確認這張票是發給「我這個服務」的,而不是同一個授權中心發給隔壁系統的票被拿來用)。


🛠️ 第 5 關|失效:JWT 最大的問題,沒有人在文章開頭告訴你

Session 的登出很單純:伺服器端把那筆 Session 資料刪掉,下次拿舊 ID 來就查無此人。

JWT 呢?JWT 是無狀態的:它一旦發出去,伺服器就管不了它了。

只要簽章有效、exp 還沒到,它就是有效的。使用者按了登出?Token 還能用。發現帳號被盜要停權?Token 還能用。管理員被降權了?他手上那張寫著 role: admin 的 Token 還是能用,直到過期為止。

這不是實作瑕疵,這是 JWT 的設計本質。實務上的處理方式是:

  1. Access Token 設得非常短(15 分鐘以內),把「失控的時間窗」壓到可接受。
  2. 搭配 Refresh Token:Refresh Token 效期長,但存在伺服器端資料庫,可以隨時撤銷。
  3. 需要立即停權的場景,準備一份撤銷清單(Blocklist)。但請注意,做到這一步,你其實已經在做「有狀態」的工作了。

🧭 所以到底該用哪一個? 這是這一篇最實際的問題:

  • 一般的 Web 應用程式(有前端頁面、有登入登出)老老實實用伺服器端 Session。它能立即撤銷、能管理閒置逾時、放在 HttpOnly Cookie 裡也不怕 XSS。
  • 服務對服務、微服務之間、短效期的 API 存取JWT 很適合,因為它的價值就在於「驗證時不必回頭查資料庫」。

很多團隊選 JWT 是因為它看起來比較現代,而不是因為他們真的需要無狀態。 而代價就是登出功能形同虛設。這是 Day 04 講的「冰山下的成本」:你以為選了一個技術,其實是選了一整組後續要處理的問題。

另外別忘了閒置逾時:使用者在公用電腦上登入後直接關掉瀏覽器,Session 應該在一段時間後自動失效,而不是等到瀏覽器關閉才處理。


🔐 補充:2025 年的密碼政策,可能跟你公司的規定完全相反

A07 不只談憑證,也談密碼政策。而這裡有一個很多人還不知道的大轉彎。NIST 在 2025 年 7 月定稿的 SP 800-63B-4 裡,用了強制性的 SHALL NOT

規定 內容
SHALL NOT 不得要求使用者定期更換密碼(除非有證據顯示已外洩)
SHALL NOT 不得強制字元組合規則(不得要求「必須含大寫、數字、特殊符號」)
SHALL 單一因素登入時,密碼最少 15 個字元;有搭配 MFA 時可放寬到 8 個字元
SHOULD 至少允許 64 個字元的長度上限
SHALL 新密碼必須比對外洩密碼清單,整組比對、不做部分比對

看到這裡,你可能會想到自己公司那條「每 90 天強制換密碼、必須含大小寫與特殊符號」的規定。那條規定按照 2025 年的標準,是明確被禁止的做法。

理由也很直白:強制輪換讓使用者從 Password1! 換成 Password2!;組合規則讓大家用同樣可預測的替換方式(a@o0)。這兩條規則都在製造「看起來很複雜、實際上很好猜」的密碼。

🛡️ 而 OWASP 對 A07 排在第一位的建議是:導入 MFA。 上面所有的密碼規則加起來,防護力都比不上多一道因素。如果你的專案只能做一件事,做這件。

最後,登入 API 一定要有速率限制。OWASP 明文把「無法阻擋自動化的憑證填充(Credential Stuffing)與暴力破解」列為 A07 的第一條判斷指標。沒有限流,前面所有防守都可以被慢慢磨開。


🎯 今日重點小結與防守心法

  • 🔹 心法 1:登入成功的那一刻,一定要換一組新的 Session ID。這行程式碼加不加,功能完全一樣,所以它永遠不會被測出來,只會被攻擊者發現。
  • 🔹 心法 2:永遠不要相信 Token 自己宣稱的東西alg 是攻擊者可以改的,所以演算法白名單必須寫死在你的程式碼裡。這跟 Day 18 的參數化是同一個精神:結構由我決定,資料才由對方提供
  • 🔹 心法 3:選 JWT 之前,先問自己「使用者按登出時會發生什麼事」。如果答案是「其實什麼都沒發生」,那你可能需要的是 Session,而不是 JWT。

💬 明日預告:【Day 21】【動手做】SSRF 伺服端請求偽造:為什麼 2025 把它併進了 A01
明天是階段三的最後一天。我們要看一個很特別的案例:一個獨立存在了四年的類別,在 2025 年被 OWASP 收編了。而理解它為什麼被收編,比背下它的防禦手法更有價值。


上一篇
【Day 19】【動手做】OWASP A02 安全設定缺陷:HTTP 安全標頭實戰
下一篇
【Day 21】【動手做】SSRF 伺服端請求偽造:為什麼 2025 把它併進了 A01
系列文
槍林彈雨下的資安防守:從品質觀念切入,帶開發者從零動手作資安 30 天22
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
AndyAWD
iT邦新手 1 級 ‧ 2026-09-08 23:46:11

我愛新密碼規定

我要留言

立即登入留言