fetchInitialCredentialsRouteWiring_audit_test.go 4.1 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
  1. package handler
  2. import (
  3. "os"
  4. "regexp"
  5. "testing"
  6. "github.com/stretchr/testify/assert"
  7. "github.com/stretchr/testify/require"
  8. )
  9. // ---------------------------------------------------------------------------
  10. // 覆盖目标:审计 M-4 的路由 wiring 静态检查 ——
  11. // `/api/product/fetchInitialCredentials` 必须挂载 `serverCtx.JwtAuth` 中间件,
  12. // 且必须位于 /api/product 前缀组内,不能被错放到其他无鉴权/错前缀块中。
  13. //
  14. // 为什么这条 wiring 需要独立钉死:
  15. // 1. routes.go 由 goctl 生成,回归时若有人用 `goctl api go -api ... -dir .`
  16. // 覆写此文件,可能把新路由吞掉或挪到无 JwtAuth 包裹的块(例如误标
  17. // `@handler` 在 pub 组)。静态检查能最早拦截。
  18. // 2. M-4 的安全假设是"只有超级管理员能消费 ticket"。RequireSuperAdmin 依赖
  19. // JwtAuth 中间件写入 UserDetails;若 JwtAuth 被去掉,handler 自身仅能回
  20. // 401(无 ctx),**但任何持有前端伪造 UserDetails 注入方式**的攻击者都会
  21. // 直接绕过 —— wiring 锚点确保这条防线永远在最外层。
  22. // ---------------------------------------------------------------------------
  23. // TC-0967: routes.go 必须把 /product/fetchInitialCredentials 挂到 JwtAuth 包裹块,
  24. // 并且处于 /api/product 前缀下(而不是 /api 或其他无超管审查的位置)。
  25. func TestRoutes_FetchInitialCredentialsJwtAuthWired(t *testing.T) {
  26. raw, err := os.ReadFile("./routes.go")
  27. require.NoError(t, err, "必须能读到 internal/handler/routes.go")
  28. src := string(raw)
  29. // go-zero 生成的 AddRoutes 块结构:
  30. // server.AddRoutes(
  31. // rest.WithMiddlewares([]rest.Middleware{serverCtx.XxxMiddleware}, []rest.Route{
  32. // {...Path: "/a"...},
  33. // {...Path: "/fetchInitialCredentials"...},
  34. // ...
  35. // }...),
  36. // rest.WithPrefix("/api/product"),
  37. // )
  38. // 我们需要:
  39. // 1. 定位到 /fetchInitialCredentials 所在的 AddRoutes 块整段;
  40. // 2. 从块里摘出 rest.Middleware{...} 列表做字符串断言;
  41. // 3. 从块里摘出 rest.WithPrefix("...") 的 prefix 做断言。
  42. // 简单起见,按"向上/向下扩展"的方式提取:以 "server.AddRoutes(" 为起点、往下到首个
  43. // "rest.WithPrefix(\"...\")" 为止的整段。
  44. addRoutesBlockRe := regexp.MustCompile(
  45. `(?s)server\.AddRoutes\(\s*rest\.WithMiddlewares\(\s*\[\]rest\.Middleware\{([^}]*)\}[\s\S]*?"/fetchInitialCredentials"[\s\S]*?rest\.WithPrefix\("([^"]+)"\)`,
  46. )
  47. m := addRoutesBlockRe.FindStringSubmatch(src)
  48. require.NotEmpty(t, m,
  49. "routes.go 中 /fetchInitialCredentials 必须位于 server.AddRoutes(rest.WithMiddlewares(...), rest.WithPrefix(...)) 结构块里;未匹配说明路由被剥离或迁移到其他结构")
  50. middlewaresList := m[1]
  51. prefix := m[2]
  52. assert.Contains(t, middlewaresList, "serverCtx.JwtAuth",
  53. "M-4:/product/fetchInitialCredentials 必须挂载 JwtAuth 中间件,否则 RequireSuperAdmin 的上下文前置条件不成立;实际中间件列表=%q", middlewaresList)
  54. assert.Equal(t, "/api/product", prefix,
  55. "M-4:/fetchInitialCredentials 必须位于 /api/product 前缀组下;实际 prefix=%q", prefix)
  56. }
  57. // TC-0968: 防御性 wiring 检查 —— 绝不允许把 fetchInitialCredentials 挂到任何
  58. // *非 JwtAuth* 的中间件块中。此用例是 TC-0967 的"反证":哪怕有人把 JwtAuth 改名
  59. // 成另一条鉴权中间件但语义错位,也会被这里拦住。
  60. func TestRoutes_FetchInitialCredentialsNotInRateLimitGroup(t *testing.T) {
  61. raw, err := os.ReadFile("./routes.go")
  62. require.NoError(t, err)
  63. src := string(raw)
  64. // 检查"限流中间件包裹块内"是否误引入了 fetchInitialCredentials
  65. for _, name := range []string{"AdminLoginRateLimit", "ProductLoginRateLimit", "RefreshTokenRateLimit", "SyncRateLimit"} {
  66. re := regexp.MustCompile(`(?s)rest\.WithMiddlewares\(\s*\[\]rest\.Middleware\{[^}]*?` + name + `[^}]*?\}[^)]*?"/fetchInitialCredentials"`)
  67. assert.False(t, re.MatchString(src),
  68. "M-4:/fetchInitialCredentials 绝不能被挂到 %s 中间件组(会绕过 JwtAuth / RequireSuperAdmin)", name)
  69. }
  70. }