JWT 深入解析:三段結構、HS256 vs RS256、常見漏洞與安全儲存
JWT(JSON Web Token)是過去十年最普及的無狀態身分驗證標準,但也是 OWASP Top 10 中最常被誤用的協定之一。從 QA 角度,理解 JWT 結構才能真正設計出有效的安全測試案例。
三段是什麼,為什麼這樣設計
JWT 用 . 切成三段,各自 Base64URL 編碼:
- Header:{"alg":"HS256","typ":"JWT"} — 演算法與型別
- Payload:{"sub":"123","exp":1700000000,"role":"admin"} — claims(主體、過期時間、自訂欄位)
- Signature:用 header 指定的演算法,把 header.payload 拿 secret(HS256)或 private key(RS256)簽出來
設計上 header / payload 沒有加密(只有編碼),任何人都能 Base64 解開看內容。真正的安全來自 signature:驗證方用 secret / public key 重算一次 signature,跟 token 帶來的比對,只要 payload 被改過,signature 就對不上。
HS256 vs RS256 vs ES256:什麼時候用哪個
- HS256(對稱):快、實作簡單,但簽章方跟驗證方共用同一個 secret。適合單一服務內部(API gateway → 自己的 backend)。 - RS256(非對稱 RSA):簽章用 private key、驗證用 public key。適合一對多場景(Auth server 發 token,多個 microservice 驗)。public key 可公開,各服務不會互相洩漏。 - ES256(非對稱 ECDSA):跟 RS256 同概念,但簽章短、計算快 80% 以上。新專案建議用這個。 WebCrypto / Node 標準函式庫都原生支援這三種。
三個必修漏洞
1. alg: none 攻擊:有的舊版 library 接受 {"alg":"none"} header,意思是「不驗 signature」。攻擊者改 payload 後把 signature 段留空,系統直接放行。
2. 演算法混淆(RS256 → HS256):server 預期收 RS256,但攻擊者把 header 改成 alg: HS256,然後用 public key 當 secret 重簽。某些 library 沒檢查 alg 一致性,public key 又是公開的,token 就被驗過。
3. 沒驗 exp / aud:大量 QA 流程只驗 signature 正不正確,忘了:
- exp claim 過期沒
- aud claim 是不是給這個服務(prevent token reuse)
- iss claim 來源對不對
設計測試案例時,這三條每條都要有獨立的負面測試。
客戶端怎麼存 token(三選一,各有風險)
- localStorage:最方便,但 XSS 一發就完蛋(惡意 script 直接讀走)。 - httpOnly + Secure + SameSite=Strict cookie:擋住 XSS,但 CSRF 風險(需配合 CSRF token 或 SameSite=Lax + 雙重驗證)。 - In-memory(React state):最安全,但刷新分頁就沒了(需要 refresh token 從 httpOnly cookie 重拿)。 業界主流:access token in memory + refresh token in httpOnly cookie,這套對 XSS 和 CSRF 都有相對好的防禦。
JWT 測試清單(QA 用)
正面測試:
- 合法 token → 200
- payload 內含的 role / scope 真的影響回傳內容
負面測試(這些 case 沒寫等於系統有漏洞):
- 過期 token → 401
- 簽章被竄改的 token → 401
- alg: none header → 401(不能是 200)
- 拿 A service 的 token 打 B service → 401(aud 驗證)
- 用 public key 當 secret 重簽 HS256 → 401(演算法混淆防禦)
- 把 payload 內的 role 從 user 改成 admin(但 signature 不改)→ 401
上面這套用 [JWT decoder / generator 工具](/zh-TW/tools/jwt) 都能手動模擬。
想實際試一下?用 [JWT 工具](/zh-TW/tools/jwt) 的 Sign 模式偽造一個 alg: none 或過期 token,丟給你的 API 看擋不擋得住。