parseWithHMAC_audit_test.go 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package auth
  2. import (
  3. "crypto/hmac"
  4. "crypto/sha256"
  5. "encoding/base64"
  6. "encoding/json"
  7. "strings"
  8. "testing"
  9. "time"
  10. "perms-system-server/internal/consts"
  11. "perms-system-server/internal/middleware"
  12. "github.com/golang-jwt/jwt/v4"
  13. "github.com/stretchr/testify/assert"
  14. "github.com/stretchr/testify/require"
  15. )
  16. // ---------------------------------------------------------------------------
  17. // 覆盖目标:审计 H-4 修复 —— ParseWithHMAC 必须显式断言 token.Method 为
  18. // *jwt.SigningMethodHMAC,拒绝任何非 HMAC 的 alg 头,包括 "none" / "RS256" 等。
  19. // 这里不等同于 jwt-go v4 对 "alg=none" 的默认拒绝,而是深度防御的显式白名单校验,
  20. // 杜绝未来迁移到 RSA/ECDSA 时攻击者把公钥当共享密钥伪造 HS256 token
  21. // (CVE-2016-10555 同类问题、OWASP JWT / RFC 8725 要求)。
  22. // ---------------------------------------------------------------------------
  23. const h4Secret = "h4-audit-secret-key"
  24. // b64url returns the jwt-style base64url (no padding) encoding.
  25. func b64url(b []byte) string { return base64.RawURLEncoding.EncodeToString(b) }
  26. // forgeToken 手动拼接一个 JWT:自定义 header.alg + payload,再用任意密钥做 HMAC 签名。
  27. // 这用于模拟"攻击者伪造头部 alg 但签名仍走 HS256"的场景。
  28. func forgeToken(t *testing.T, alg string, claims any, signingKey string) string {
  29. t.Helper()
  30. header := map[string]string{"alg": alg, "typ": "JWT"}
  31. hBytes, err := json.Marshal(header)
  32. require.NoError(t, err)
  33. pBytes, err := json.Marshal(claims)
  34. require.NoError(t, err)
  35. signingInput := b64url(hBytes) + "." + b64url(pBytes)
  36. mac := hmac.New(sha256.New, []byte(signingKey))
  37. mac.Write([]byte(signingInput))
  38. sig := mac.Sum(nil)
  39. return signingInput + "." + b64url(sig)
  40. }
  41. // forgeTokenNoSig 拼接一个没有签名的 token(alg=none 典型攻击,第三段签名留空)。
  42. func forgeTokenNoSig(t *testing.T, alg string, claims any) string {
  43. t.Helper()
  44. header := map[string]string{"alg": alg, "typ": "JWT"}
  45. hBytes, err := json.Marshal(header)
  46. require.NoError(t, err)
  47. pBytes, err := json.Marshal(claims)
  48. require.NoError(t, err)
  49. return b64url(hBytes) + "." + b64url(pBytes) + "."
  50. }
  51. // validRefreshClaims 返回一组完整、未过期的 refresh claims,用于伪造攻击 token。
  52. func validRefreshClaims() RefreshClaims {
  53. now := time.Now()
  54. return RefreshClaims{
  55. TokenType: consts.TokenTypeRefresh,
  56. UserId: 7,
  57. ProductCode: "h4_pc",
  58. TokenVersion: 0,
  59. RegisteredClaims: jwt.RegisteredClaims{
  60. ExpiresAt: jwt.NewNumericDate(now.Add(1 * time.Hour)),
  61. IssuedAt: jwt.NewNumericDate(now),
  62. },
  63. }
  64. }
  65. // TC-0951: H-4 —— 正常 HS256 token 必须被 ParseWithHMAC 正确接受。
  66. func TestParseWithHMAC_HS256_Valid(t *testing.T) {
  67. tok, err := GenerateRefreshToken(h4Secret, 3600, 7, "h4_pc", 0)
  68. require.NoError(t, err)
  69. token, err := ParseWithHMAC(tok, h4Secret, &RefreshClaims{})
  70. require.NoError(t, err)
  71. assert.True(t, token.Valid)
  72. claims, ok := token.Claims.(*RefreshClaims)
  73. require.True(t, ok)
  74. assert.Equal(t, int64(7), claims.UserId)
  75. assert.Equal(t, consts.TokenTypeRefresh, claims.TokenType)
  76. }
  77. // TC-0952: H-4 —— alg=none 的伪造 token 必须被拒绝。
  78. // jwt-go v4 默认就会拦住 "none",但显式 HMAC 断言保证即使 lib 行为变化我们仍 fail-close。
  79. func TestParseWithHMAC_AlgNone_Rejected(t *testing.T) {
  80. forged := forgeTokenNoSig(t, "none", validRefreshClaims())
  81. _, err := ParseWithHMAC(forged, h4Secret, &RefreshClaims{})
  82. require.Error(t, err, "alg=none 必须被 ParseWithHMAC 拒绝")
  83. }
  84. // TC-0953: H-4 —— 攻击者把 header alg 改成 RS256 但仍用 secret 作 HS256 签名
  85. // (RSA 公钥 → HMAC secret 混淆攻击)。必须被 ParseWithHMAC 显式拒绝:
  86. // 命中 keyfunc 的 `token.Method.(*SigningMethodHMAC)` 断言失败分支。
  87. func TestParseWithHMAC_RS256HeaderButHMACSigned_Rejected(t *testing.T) {
  88. forged := forgeToken(t, "RS256", validRefreshClaims(), h4Secret)
  89. _, err := ParseWithHMAC(forged, h4Secret, &RefreshClaims{})
  90. require.Error(t, err, "alg=RS256 必须被 ParseWithHMAC 拒绝")
  91. assert.Contains(t, err.Error(), "unexpected signing method",
  92. "错误信息必须明确指出 alg 与预期不符(便于运维快速定位攻击尝试)")
  93. }
  94. // TC-0954: H-4 —— alg=ES256 同样应被拒绝(非 HMAC 算法一律拒绝)。
  95. func TestParseWithHMAC_ES256HeaderButHMACSigned_Rejected(t *testing.T) {
  96. forged := forgeToken(t, "ES256", validRefreshClaims(), h4Secret)
  97. _, err := ParseWithHMAC(forged, h4Secret, &RefreshClaims{})
  98. require.Error(t, err)
  99. assert.Contains(t, err.Error(), "unexpected signing method")
  100. }
  101. // TC-0955: H-4 —— alg=HS256 但用错误的 secret 签名应被拒绝(签名校验失败路径)。
  102. func TestParseWithHMAC_HS256WrongSecret_Rejected(t *testing.T) {
  103. tok, err := GenerateRefreshToken("attacker-guessed-secret", 3600, 7, "h4_pc", 0)
  104. require.NoError(t, err)
  105. _, err = ParseWithHMAC(tok, h4Secret, &RefreshClaims{})
  106. require.Error(t, err, "签名校验失败必须回错,不得放行")
  107. }
  108. // TC-0956: H-4 —— ParseRefreshToken(对外真实入口)也走 HMAC 断言,alg=RS256 必须被拒。
  109. // 保证 ParseWithHMAC 不是孤立函数,而是已被真实调用链使用。
  110. func TestParseRefreshToken_RS256Header_Rejected(t *testing.T) {
  111. forged := forgeToken(t, "RS256", validRefreshClaims(), h4Secret)
  112. _, err := ParseRefreshToken(forged, h4Secret)
  113. require.Error(t, err, "ParseRefreshToken 必须转交 ParseWithHMAC 拒绝 RS256 伪造 token")
  114. }
  115. // TC-0957: H-4 —— ParseRefreshToken 对 alg=none 的 token 也必须拒绝。
  116. func TestParseRefreshToken_AlgNone_Rejected(t *testing.T) {
  117. forged := forgeTokenNoSig(t, "none", validRefreshClaims())
  118. _, err := ParseRefreshToken(forged, h4Secret)
  119. require.Error(t, err)
  120. }
  121. // TC-0958: H-4 回归 —— 格式错误的 token(非三段式)必须 error 而不是 panic。
  122. func TestParseWithHMAC_Malformed_Rejected(t *testing.T) {
  123. cases := []string{
  124. "",
  125. "not-a-token",
  126. "only.two",
  127. "a.b.c.d", // 四段
  128. }
  129. for _, s := range cases {
  130. t.Run("malformed:"+s, func(t *testing.T) {
  131. _, err := ParseWithHMAC(s, h4Secret, &RefreshClaims{})
  132. require.Error(t, err)
  133. })
  134. }
  135. }
  136. // TC-0959: H-4 —— payload 中 TokenType 非 refresh 的 HS256 token 应被 ParseRefreshToken
  137. // 以 ErrTokenTypeMismatch 拒绝。确认 H-4 修复不会误吞该业务校验。
  138. func TestParseRefreshToken_AccessTokenRejectedWithTypeMismatch(t *testing.T) {
  139. accessTok, err := GenerateAccessToken(h4Secret, 3600, 7, "u", "p", "M", 0)
  140. require.NoError(t, err)
  141. _, err = ParseRefreshToken(accessTok, h4Secret)
  142. require.Error(t, err)
  143. assert.Equal(t, ErrTokenTypeMismatch, err,
  144. "H-4 的 ParseWithHMAC 不能吞掉业务层 TokenType 校验错误")
  145. }
  146. // TC-0960: H-4 —— 伪造 alg=HS256 但 header.typ 异常(如 "JWT"→"xxx")也不能绕过
  147. // HMAC 校验。此用例用来证明只要底层签名正确,header 其余字段不影响放行/拒绝的核心语义。
  148. // 反之,任何 alg 头不是 HS* 的一律拒,和 typ 无关。
  149. func TestParseWithHMAC_HS256UnusualTyp_Accepted(t *testing.T) {
  150. // header.alg = HS256, header.typ = "JWT+weird",签名正确 → 应放行(typ 不参与断言)
  151. header := map[string]string{"alg": "HS256", "typ": "JWT+weird"}
  152. hBytes, _ := json.Marshal(header)
  153. claims := validRefreshClaims()
  154. pBytes, _ := json.Marshal(claims)
  155. signingInput := b64url(hBytes) + "." + b64url(pBytes)
  156. mac := hmac.New(sha256.New, []byte(h4Secret))
  157. mac.Write([]byte(signingInput))
  158. tok := signingInput + "." + b64url(mac.Sum(nil))
  159. _, err := ParseWithHMAC(tok, h4Secret, &RefreshClaims{})
  160. require.NoError(t, err,
  161. "HMAC 断言只看 alg,typ 不属于签名算法白名单范畴,正常 HS256 应放行")
  162. }
  163. // 辅助:保持 strings 导入被使用,避免 go vet 警告。
  164. var _ = strings.Split
  165. // 确保 middleware.Claims 在包内可被用于 TypeRefresh / TypeAccess 等正反测试(未来扩展)。
  166. var _ = middleware.Claims{}