permserver_fuzz_audit_test.go 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. package server
  2. import (
  3. "context"
  4. "testing"
  5. "perms-system-server/internal/svc"
  6. "perms-system-server/internal/testutil"
  7. "perms-system-server/pb"
  8. "google.golang.org/grpc/codes"
  9. "google.golang.org/grpc/status"
  10. )
  11. // TC-0794: gRPC VerifyToken 契约层 fuzz —— 任意畸形 AccessToken 必须:
  12. // (1) 返回 (resp, nil) 而非 panic / (nil, err), 因为签名/过期/payload 问题在对外契约里都只是"无效令牌"
  13. // (2) 当返回时 resp.Valid 必须为 false (不能误把 nil token 判成有效)
  14. //
  15. // 种子覆盖常见的攻击 payload: 空串、非 JWT 结构、alg=none 试探、超长串、Unicode 噪声、控制字符。
  16. // 在 CI 里 `go test -run ^FuzzVerifyToken$` 只会执行种子语料, 不会触发随机变异, 因此确定性高、耗时可控。
  17. // 本地做 `go test -fuzz=FuzzVerifyToken -fuzztime=30s` 可进一步跑随机变异。
  18. func FuzzVerifyToken_NeverPanicsAlwaysInvalid(f *testing.F) {
  19. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  20. srv := NewPermServer(svcCtx)
  21. seeds := []string{
  22. "",
  23. " ",
  24. ".",
  25. "..",
  26. "not.a.jwt",
  27. "a.b.c",
  28. "eyJhbGciOiJub25lIn0.eyJ1c2VySWQiOjF9.", // alg=none 试探
  29. "Bearer xxx",
  30. "null",
  31. "\x00\x01\x02",
  32. "🔥token💥",
  33. string(make([]byte, 4096)), // 长令牌
  34. "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjF9.sig", // 伪造 HS256
  35. }
  36. for _, s := range seeds {
  37. f.Add(s)
  38. }
  39. f.Fuzz(func(t *testing.T, raw string) {
  40. defer func() {
  41. if r := recover(); r != nil {
  42. t.Fatalf("VerifyToken panicked on input=%q: %v", raw, r)
  43. }
  44. }()
  45. resp, err := srv.VerifyToken(context.Background(), &pb.VerifyTokenReq{AccessToken: raw})
  46. if err != nil {
  47. t.Fatalf("VerifyToken must never return an error for malformed input, got err=%v (input=%q)", err, raw)
  48. }
  49. if resp == nil {
  50. t.Fatalf("VerifyToken must return non-nil response (input=%q)", raw)
  51. }
  52. if resp.Valid {
  53. t.Fatalf("malformed/invalid token must never be reported valid; input=%q", raw)
  54. }
  55. })
  56. }
  57. // TC-0795: gRPC GetUserPerms 契约层 fuzz —— 任意 (appKey, appSecret, productCode, userId) 组合下:
  58. // (1) 必须返回 status.Error(非 200); 不允许 panic / nil error + 有权限返回
  59. // (2) 错误码必须落在固定集合内: Unauthenticated / PermissionDenied / InvalidArgument / NotFound / Internal
  60. // —— 否则契约漂移, 产品侧"权限网关"无法稳定处理
  61. //
  62. // 此用例不需要预置任何数据, 专打输入校验/认证失败的快速拒绝路径。
  63. func FuzzGetUserPerms_ErrorTaxonomyStable(f *testing.F) {
  64. svcCtx := svc.NewServiceContext(testutil.GetTestConfig())
  65. srv := NewPermServer(svcCtx)
  66. seeds := [][4]string{
  67. {"", "", "", ""},
  68. {"nonexistent_appkey_" + testutil.UniqueId(), "x", "p", "1"},
  69. {"appkey", "wrong_secret", "code", "0"},
  70. {"🔑", "🔒", "😈", "-1"},
  71. {"'; DROP TABLE sys_product; --", "s", "p", "1"},
  72. {string(make([]byte, 512)), "s", "p", "1"},
  73. }
  74. for _, s := range seeds {
  75. f.Add(s[0], s[1], s[2], s[3])
  76. }
  77. allowed := map[codes.Code]bool{
  78. codes.Unauthenticated: true,
  79. codes.PermissionDenied: true,
  80. codes.InvalidArgument: true,
  81. codes.NotFound: true,
  82. codes.Internal: true,
  83. }
  84. f.Fuzz(func(t *testing.T, appKey, appSecret, productCode, userIdStr string) {
  85. defer func() {
  86. if r := recover(); r != nil {
  87. t.Fatalf("GetUserPerms panicked on input=(%q,%q,%q,%q): %v", appKey, appSecret, productCode, userIdStr, r)
  88. }
  89. }()
  90. var uid int64
  91. for _, c := range userIdStr {
  92. if c >= '0' && c <= '9' {
  93. uid = uid*10 + int64(c-'0')
  94. if uid > 1e15 {
  95. break
  96. }
  97. }
  98. }
  99. _, err := srv.GetUserPerms(context.Background(), &pb.GetUserPermsReq{
  100. AppKey: appKey, AppSecret: appSecret, ProductCode: productCode, UserId: uid,
  101. })
  102. if err == nil {
  103. t.Fatalf("malformed/unauthenticated input must produce an error; appKey=%q", appKey)
  104. }
  105. st, ok := status.FromError(err)
  106. if !ok {
  107. t.Fatalf("error must be a grpc status.Error, got %T (%v)", err, err)
  108. }
  109. if !allowed[st.Code()] {
  110. t.Fatalf("error code %s is outside the agreed contract taxonomy; must be one of Unauthenticated/PermissionDenied/InvalidArgument/NotFound/Internal. msg=%q",
  111. st.Code(), st.Message())
  112. }
  113. })
  114. }