package logger import ( "bytes" "io" "net/http/httptest" "strings" "testing" "github.com/gofiber/fiber/v2" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) func TestSanitizeJSONRecursivelyMasksRequestAndResponseSecrets(t *testing.T) { t.Parallel() raw := []byte(`{"Password":"p@ss","items":[{"access_token":"token-value","count":3}],"profile":{"private_url":"https://secret.example/a"}}`) result := sanitizeBody(raw, BodyPolicyJSON) if strings.Contains(result.Content, "p@ss") || strings.Contains(result.Content, "token-value") || strings.Contains(result.Content, "secret.example") { t.Fatalf("访问日志仍包含敏感值:%s", result.Content) } if !strings.Contains(result.Content, redactedValue) { t.Fatalf("访问日志未输出统一脱敏占位:%s", result.Content) } if result.Truncated { t.Fatal("短载荷不应标记为截断") } } func TestFiberAccessLogSanitizesFinalRequestAndResponseJSON(t *testing.T) { var output bytes.Buffer encoderConfig := zap.NewProductionEncoderConfig() log := zap.New(zapcore.NewCore( zapcore.NewJSONEncoder(encoderConfig), zapcore.AddSync(&output), zapcore.InfoLevel, )) app := fiber.New() app.Use(func(c *fiber.Ctx) error { c.Locals("requestid", "request-test-1") return c.Next() }) app.Use(MiddlewareWithLogger(log)) app.Post("/tokens", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"access_token": "response-token", "data": fiber.Map{"ok": true}}) }) request := httptest.NewRequest("POST", "/tokens?Authorization=query-secret&page=1", strings.NewReader(`{"password":"request-secret"}`)) request.Header.Set("Content-Type", "application/json") response, err := app.Test(request) if err != nil { t.Fatalf("执行 Fiber 请求失败:%v", err) } _, _ = io.Copy(io.Discard, response.Body) _ = response.Body.Close() logged := output.String() for _, secret := range []string{"request-secret", "response-token", "query-secret"} { if strings.Contains(logged, secret) { t.Fatalf("最终 JSON 访问日志泄露测试凭证 %q:%s", secret, logged) } } for _, field := range []string{"request_id", "status", "duration_ms", "request_body_truncated", "response_body_truncated"} { if !strings.Contains(logged, field) { t.Fatalf("最终 JSON 访问日志缺少字段 %q:%s", field, logged) } } } func TestSensitiveRouteMatrixNeverLogsRawPayload(t *testing.T) { cases := []struct { name string path string contentType string body string secret string }{ {name: "登录JSON", path: "/api/auth/login", contentType: "application/json", body: `{"username":"safe-user","password":"json-secret"}`, secret: "json-secret"}, {name: "企微XML", path: "/api/callback/wecom", contentType: "application/xml", body: `xml-secret`, secret: "xml-secret"}, {name: "支付表单", path: "/api/callback/alipay", contentType: "application/x-www-form-urlencoded", body: "order_no=SAFE-1&sign=form-secret", secret: "form-secret"}, {name: "文件multipart", path: "/api/admin/storage/upload", contentType: "multipart/form-data; boundary=x", body: "--x\r\nContent-Disposition: form-data; name=\"file\"; filename=\"secret.bin\"\r\nContent-Type: application/octet-stream\r\n\r\nfile-bytes-secret\r\n--x--\r\n", secret: "file-bytes-secret"}, {name: "导出二进制", path: "/api/admin/export-tasks/1/download", contentType: "application/octet-stream", body: "binary-secret", secret: "binary-secret"}, {name: "支付不可解析", path: "/api/callback/wechat-pay", contentType: "application/json", body: `{"sign":"broken-secret"`, secret: "broken-secret"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { var output bytes.Buffer log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel)) app := fiber.New() app.Use(MiddlewareWithLogger(log)) app.Post(tc.path, func(c *fiber.Ctx) error { return c.Status(fiber.StatusOK).Type("json").SendString(`{"access_token":"response-secret","status":"ok"}`) }) request := httptest.NewRequest("POST", tc.path+"?signature=query-secret", strings.NewReader(tc.body)) request.Header.Set("Content-Type", tc.contentType) request.Header.Set("Authorization", "Bearer header-secret") request.Header.Set("Cookie", "session=cookie-secret") response, err := app.Test(request) if err != nil { t.Fatalf("执行敏感路由请求失败:%v", err) } _, _ = io.Copy(io.Discard, response.Body) _ = response.Body.Close() logged := output.String() for _, secret := range []string{tc.secret, "query-secret", "response-secret", "header-secret", "cookie-secret"} { if strings.Contains(logged, secret) { t.Fatalf("敏感路由日志泄露 %q:%s", secret, logged) } } if !strings.Contains(logged, "body_policy") || !strings.Contains(logged, "sha256") { t.Fatalf("敏感路由日志缺少策略或摘要:%s", logged) } }) } } func TestSensitiveRouteLongBodyRecordsTruncationWithoutRawSecret(t *testing.T) { var output bytes.Buffer log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel)) app := fiber.New() app.Use(MiddlewareWithLogger(log)) app.Post("/api/admin/storage/upload", func(c *fiber.Ctx) error { return c.SendStatus(fiber.StatusOK) }) secret := strings.Repeat("long-file-secret", MaxBodyLogSize) request := httptest.NewRequest("POST", "/api/admin/storage/upload", strings.NewReader(secret)) request.Header.Set("Content-Type", "application/octet-stream") response, err := app.Test(request, 10000) if err != nil { t.Fatalf("执行超长敏感请求失败:%v", err) } _ = response.Body.Close() logged := output.String() if strings.Contains(logged, "long-file-secret") || !strings.Contains(logged, `"request_body_truncated":true`) { t.Fatalf("超长敏感载荷未安全摘要或未标记截断:%s", logged) } } func TestRealConfigurationRoutesOnlyLogSafeSummary(t *testing.T) { for _, path := range []string{ "/api/admin/system-configs/payment.private_key", "/api/admin/wechat-configs/1", } { t.Run(path, func(t *testing.T) { var output bytes.Buffer log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel)) app := fiber.New() app.Use(MiddlewareWithLogger(log)) app.Put(path, func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"value": "response-private-material", "status": "ok"}) }) request := httptest.NewRequest("PUT", path, strings.NewReader(`{"value":"request-private-material","config_key":"payment.private_key"}`)) request.Header.Set("Content-Type", "application/json") response, err := app.Test(request) if err != nil { t.Fatalf("执行配置路由请求失败:%v", err) } _ = response.Body.Close() logged := output.String() for _, secret := range []string{"request-private-material", "response-private-material"} { if strings.Contains(logged, secret) { t.Fatalf("配置路由日志泄露 %q:%s", secret, logged) } } if !strings.Contains(logged, `"body_policy":"sensitive_config"`) { t.Fatalf("配置路由未应用安全摘要策略:%s", logged) } if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"length\":`) { t.Fatalf("配置路由未记录字段存在性和长度:%s", logged) } }) } } func TestLoginRouteDoesNotLogUsernameAndOnlyKeepsPresenceLength(t *testing.T) { var output bytes.Buffer log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel)) app := fiber.New() app.Use(MiddlewareWithLogger(log)) app.Post("/api/auth/login", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"username": "visible-user", "success": true}) }) request := httptest.NewRequest("POST", "/api/auth/login", strings.NewReader(`{"username":"visible-user","password":"login-secret"}`)) request.Header.Set("Content-Type", "application/json") response, err := app.Test(request) if err != nil { t.Fatalf("执行登录请求失败:%v", err) } _ = response.Body.Close() logged := output.String() if strings.Contains(logged, "visible-user") || strings.Contains(logged, "login-secret") { t.Fatalf("登录路由泄露账号或凭证:%s", logged) } if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"length\":`) { t.Fatalf("登录路由未记录字段存在性和长度:%s", logged) } } func TestPaymentRouteOnlyLogsPresenceLengthAndSafeResult(t *testing.T) { var output bytes.Buffer log := zap.New(zapcore.NewCore(zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), zapcore.AddSync(&output), zapcore.InfoLevel)) app := fiber.New() app.Use(MiddlewareWithLogger(log)) app.Post("/api/callback/alipay", func(c *fiber.Ctx) error { return c.JSON(fiber.Map{"payment_no": "PAY-SECRET-1", "amount": 12345, "status": "success"}) }) request := httptest.NewRequest("POST", "/api/callback/alipay", strings.NewReader("order_no=ORDER-SECRET-1&amount=12345&sign=signature-secret")) request.Header.Set("Content-Type", "application/x-www-form-urlencoded") response, err := app.Test(request) if err != nil { t.Fatalf("执行支付路由请求失败:%v", err) } _ = response.Body.Close() logged := output.String() for _, value := range []string{"PAY-SECRET-1", "ORDER-SECRET-1", "signature-secret"} { if strings.Contains(logged, value) { t.Fatalf("支付路由泄露原值 %q:%s", value, logged) } } if !strings.Contains(logged, `\"present\":true`) || !strings.Contains(logged, `\"status\":\"success\"`) { t.Fatalf("支付路由缺少存在性摘要或安全结果:%s", logged) } } func TestSanitizeInvalidJSONNeverFallsBackToRawBody(t *testing.T) { t.Parallel() raw := []byte(`{"password":"should-never-appear"`) result := sanitizeBody(raw, BodyPolicyJSON) if strings.Contains(result.Content, "should-never-appear") { t.Fatalf("解析失败时不得回退原文:%s", result.Content) } if result.SHA256 == "" || result.Size != len(raw) { t.Fatalf("解析失败摘要缺少大小或哈希:%+v", result) } } func TestSanitizeQueryUsesSameSensitiveFieldRules(t *testing.T) { t.Parallel() query := sanitizeQuery("page=1&Authorization=Bearer-secret&nonce=abc") if strings.Contains(query.Content, "Bearer-secret") || strings.Contains(query.Content, "abc") { t.Fatalf("query 敏感值未脱敏:%s", query.Content) } }