JWT 常見陷阱與最佳實務
JWT 的五個常見地雷——alg=none、HMAC 密鑰過弱、缺少 exp、盲信 kid、混用 JWS 與 JWE——以及在 2026 年該如何各個擊破。
JWT——JSON Web Token——是 2026 年 HTTP API 的預設權杖格式。規格很 小(RFC 7519)、函式庫到處都有,一個權杖不過是用點號連起來的三段 base64url。看似簡單,正是問題所在:本文所列的每一個地雷,都曾在你聽 過名字的公司上線過,而且往往不只一次。只要知道要找什麼,防禦其實 很直接。
本文走過五個反覆出現的失敗模式——alg=none、HMAC 密鑰長度不足、
缺少 exp、把 kid 當路徑信任、混淆 JWS 與 JWE——並在結尾給出新
系統的演算法選擇建議。
alg=none——最原始的罪
2015 年 Tim McLean 公開了針對早期 JWT 函式庫的 alg=none 攻擊。JWT
header 宣告了簽章演算法,而某一類函式庫就無條件地相信這個宣告。攻擊
者送出 header 為 {"alg":"none"}、signature 段為空的權杖,函式庫
便乖乖跳過驗證、把它視為合法。Auth0、node-jsonwebtoken、pyjwt 及一
長串分支都中過同一個 bug 的不同版本。
RFC 8725(JWT 最佳實務,2020)把這條列為第一項對策:該由驗證端 而非權杖本身決定可接受的演算法。具體做法是每個呼叫端都顯式傳入 允許清單:
// vulnerable: trusts the alg header
const payload = jwt.verify(token, key);
// fixed: pin the algorithm
const payload = jwt.verify(token, key, { algorithms: ["RS256"] });
同一條規則也適用於演算法混淆攻擊:把以 HS256 簽的權杖丟給原本預期
RS256 的驗證端。若驗證端「依 header 的 alg 決定使用的程序」,就會把
公鑰當作 HMAC 密鑰雜湊——而任何人(公鑰依定義是公開的)都能藉此偽造
權杖。請把演算法寫死。
HMAC 密鑰過弱
HS256 是對稱的:同一把密鑰簽署、同一把密鑰驗證。RFC 7518 §3.2 寫
得很清楚——密鑰長度至少要等於 HMAC 輸出長度,也就是 HS256 至少
32 bytes、HS384 至少 48、HS512 至少 64。六個字元的密碼不是密鑰,
而是一台筆電就能在五分鐘內離線爆破的對象。網路上已有公開工具,能
在幾秒內以字典檔破解短密鑰簽的 JWT。
請用完整熵生成密鑰,並把它放進密鑰管理工具:
# 32 random bytes, base64url-encoded — paste into your secret store
openssl rand -base64 32 | tr '+/' '-_' | tr -d '='
// Node — produce a fresh HS256 secret programmatically
import { randomBytes } from "node:crypto";
const secret = randomBytes(32); // raw bytes; encode for storage
絕不要把密鑰簽進原始碼、不要從環境變數名稱推導出來,也要定期輪替—— RFC 8725 §2.1 明確建議要輪替。
缺少 exp
沒有過期時間的 JWT,就是一張永久通行證。一旦外洩——忘了清的 log、 瀏覽器擴充功能、設錯的 proxy——你唯一能讓它失效的方法,是輪替簽署 密鑰,這會讓 所有 權杖一起失效,通常意味著全站登出。這不是一個 好的事故應變方案。
挑一個與爆炸半徑相稱的時窗:
- Access token:5–15 分鐘。短到外洩之後能自動癒合。
- Refresh token:數小時到數天,只儲存於可信客戶端,使用時輪替。
- ID token(OIDC):分鐘等級;它在登入時證明身分,不該被重用。
伺服器端永遠要驗證 exp。有些函式庫預設會驗、有些不會——即便預設
會驗,也通常允許在測試時關閉。請審視你所有的 verify 呼叫點:
const claims = jwt.verify(token, key, {
algorithms: ["RS256"],
// do NOT pass { ignoreExpiration: true } in production
});
如果你需要長時效的授權(CLI 工具、CI runner),請發行短時效的 JWT 加
上獨立追蹤的 refresh token——不要把 exp 拉長到一年。
盲信 kid
kid(key id)是用於密鑰輪替的 header:簽發端在每個權杖上標記簽署
密鑰的 id,驗證端據此查出對應的公鑰,輪替就能不停機完成。陷阱在於
kid 是 header 的一部分,意思就是它由攻擊者控制。請當成不可信
輸入處理。
經典的失敗:
// path traversal — kid becomes a filesystem path
const key = fs.readFileSync(`/etc/keys/${header.kid}.pem`);
// attacker sends kid = "../../../../dev/null" or a public path
// SQL injection — kid becomes a query parameter
const key = await db.query(`SELECT pem FROM keys WHERE id = '${header.kid}'`);
// URL fetch — kid becomes a remote endpoint
const key = await fetch(header.kid).then(r => r.text());
// SSRF against internal services, or attacker-hosted "public key"
修法與處理任何其他不可信識別碼一致:在白名單中查找。維護一份以
kid 為索引的金鑰集合,凡是 kid 不在集合中的權杖一律拒絕,更
不要讓 kid 的值流到檔案系統呼叫、資料庫驅動或 HTTP client。對於
OIDC,JWKS URI 是固定設定;你抓回完整的集合、再依 kid 在結果中
挑選,而非反過來。
JWS 與 JWE——挑對的那一個
JWT 只是 payload 的形狀;外層包裝是 JWS(簽章)或 JWE(加密)。兩者 解決的是不同問題,而人們經常拿錯。
- JWS 證明 誰 簽發了權杖。任何人都讀得到 payload——只要把中段 做 base64url 解碼即可。這是完整性,不是機密性。
- JWE 證明 payload 是什麼——整段都被加密。接收端需要解密金鑰; 其他人只看得到密文。
若你的權杖只攜帶 user id 與 expiry,JWS 就對了:接收端驗章後讀出 claim。 若權杖攜帶 session secret、不該讓使用者看見的功能旗標,或不希望寫入 log 的 PII,JWS 就不夠了。請改用 JWE,或者——更常見的做法——根本 不要把敏感資料放在權杖中,改在伺服器端以一個 opaque 的 session id 索引。
當你真的同時需要簽章與加密(一個已簽署的權杖要機密地送給已知接收端), 規格的標準解答是 巢狀 JWT:先簽章,再把 JWS 當成 JWE 的 payload 加 密。直接把未簽署的 payload 加密,會得到「有機密性但無認證」的東西, 這通常不是你要的。
驗證權杖
對於一次性的除錯——log 中撈到的權杖、curl 回應的 header、廠商給的範例
——把它貼進 JWT 解碼與驗證工具。工具會在不驗章的情況下
先解出 header 與 payload(對任何權杖都安全),再依你提供的金鑰選擇性
地驗章。它實作了完整的演算法矩陣(HS/RS/PS/ES/EdDSA),並支援以
A128/192/256GCM 與常見金鑰包裝演算法解密 JWE。你貼上的當下,警告引擎
就會標出 alg=none、HMAC 密鑰過弱、權杖過期等本文提到的地雷。
請把它當成診斷工具,而不是生產程式碼的驗證步驟——生產驗證應該寫在你 服務內部的函式庫中,演算法寫死、金鑰從密鑰管理工具載入。
2026 年的演算法選擇
如果你今天要動工一個新系統,這是一個簡短的意見:
- EdDSA(Ed25519) 是非對稱權杖的最佳預設值。金鑰短(32 bytes)、
簽章短(64 bytes)、決定性、沒有參數陷阱、驗證很快。
jose、golang.org/x/crypto/ed25519、Pythoncryptography以及多數現代 JWT 函式庫都已支援。 - RS256 在 OIDC 提供者與舊式 SaaS 整合中仍是最具互通性的選擇。 金鑰較大(至少 2048 bits,新金鑰建議 3072)但所有 JWT 函式庫都支援。 當你要發布給任意客戶端消費的 JWKS 時,請選 RS256。
- HS256 適合服務對服務的權杖,前提是兩端都在你掌控之下、共享密鑰 能放在 vault 中管理。它不適合跨越信任邊界的權杖——任何拿到驗證金鑰 的人都能偽造權杖。
- ES256 在 EdDSA 不可用時是合理的中間選項;它比 EdDSA 慢,且歷史上
在隨機
k值的實作上有過陷阱,但現代函式庫已能正確處理。
請避開 RS1、HS1,以及任何鎖在 SHA-1 上的東西——SHA-1 的碰撞抗性早
已破解,2026 年沒有理由再上線一個以 SHA-1 為基礎的權杖。none 演算法
全面禁用,包括測試 fixture,避免有人不小心把測試樁推到生產環境。
結尾檢查清單
JWT 驗證上線前的檢查:
- verify 呼叫中寫死演算法。不依 header 退回。
- HMAC 密鑰至少 32 bytes,以 CSPRNG 生成。
- 非對稱金鑰來自 JWKS 或固定的 PEM,而非
kid。 - 每個權杖都有
exp,驗證端會強制檢查。 -
iss與aud都與預期值比對。 - 若權杖是純簽章的 JWS,payload 中不放敏感資料。
- 測試 fixture 使用真實演算法,絕不用
none。
防禦面其實很淺——半打不變量,全部都能在共用 helper 中強制。一旦失守, 攻擊面就是完整的帳號接管。值得每隔一段時間審計一次。