@sun-asterisk/sunlint 1.3.48 → 1.3.49
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/core/file-targeting-service.js +148 -15
- package/core/init-command.js +118 -70
- package/core/project-detector.js +517 -0
- package/core/tui-select.js +245 -0
- package/engines/arch-detect/rules/layered/l001-presentation-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l002-business-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l003-data-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l004-model-layer.js +7 -15
- package/engines/arch-detect/rules/layered/l005-layer-separation.js +22 -2
- package/engines/arch-detect/rules/layered/l006-dependency-direction.js +8 -5
- package/engines/arch-detect/rules/modular/m005-no-deep-imports.js +67 -29
- package/engines/arch-detect/rules/presentation/pr001-view-layer.js +16 -9
- package/engines/arch-detect/rules/presentation/pr006-router-layer.js +33 -8
- package/engines/arch-detect/rules/presentation/pr007-interactor-layer.js +35 -6
- package/engines/arch-detect/rules/project-scanner/ps003-framework-detection.js +56 -10
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/dart/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C013-no-dead-code.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C014-dependency-injection.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C017-no-constructor-logic.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C018-generic-errors.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C020-no-unused-imports.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C022-no-unused-variables.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C023-no-duplicate-names.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C024-centralize-constants.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C029-catch-log-root-cause.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C030-custom-error-classes.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C033-separate-data-access.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C035-error-context-logging.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C041-no-hardcoded-secrets.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C042-boolean-naming.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C052-widget-parsing.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C060-superclass-logic.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/dart/C067-no-hardcoded-config.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/AGENTS.md +149 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN001-abort-after-response.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN002-request-context.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN003-bind-error-handling.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN004-dependency-injection.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN005-route-groups-middleware.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN006-http-status-codes.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN007-release-mode.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN008-struct-validation-tags.md +90 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN009-recovery-middleware.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN010-context-scope.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN011-middleware-concerns.md +92 -0
- package/skill-assets/sunlint-code-quality/rules/go-gin/GN012-no-log-sensitive.md +84 -0
- package/skill-assets/sunlint-code-quality/rules/java/J001-try-with-resources.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/java/J002-equals-and-hashcode.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J003-string-comparison.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/java/J004-use-java-time.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J005-no-print-stack-trace.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/java/J006-no-system-println.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/java/J007-proper-logger.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/java/J008-thread-safe-singleton.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J009-utility-class-constructor.md +82 -0
- package/skill-assets/sunlint-code-quality/rules/java/J010-preserve-stack-trace.md +119 -0
- package/skill-assets/sunlint-code-quality/rules/java/J011-null-safe-compare.md +88 -0
- package/skill-assets/sunlint-code-quality/rules/java/J012-use-enum-collections.md +104 -0
- package/skill-assets/sunlint-code-quality/rules/java/J013-return-empty-not-null.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/java/J014-hardcoded-crypto-key.md +108 -0
- package/skill-assets/sunlint-code-quality/rules/java/J015-optional-instead-of-null.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/AGENTS.md +124 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV001-form-request-validation.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV002-eager-load-no-n-plus-1.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV003-config-not-env.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV004-fillable-mass-assignment.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV005-policies-gates-authorization.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV006-queue-heavy-tasks.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV007-hash-passwords.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV008-route-model-binding.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV009-api-resources.md +72 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV010-chunk-large-datasets.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV011-db-transactions.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV012-service-layer.md +78 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV013-testing-factories.md +75 -0
- package/skill-assets/sunlint-code-quality/rules/php-laravel/LV014-service-container.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/python/P001-mutable-default-argument.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/python/P002-specify-file-encoding.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/python/P003-context-manager-for-resources.md +54 -0
- package/skill-assets/sunlint-code-quality/rules/python/P004-no-bare-except.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/python/P005-use-isinstance.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/python/P006-timezone-aware-datetime.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/python/P007-use-pathlib.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/python/P008-no-wildcard-import.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/python/P009-logging-lazy-format.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/python/P010-exception-chaining.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/python/P011-subprocess-check.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/python/P012-requests-timeout.md +70 -0
- package/skill-assets/sunlint-code-quality/rules/python/P013-no-global-statement.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/python/P014-no-modify-collection-while-iterating.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/python/P015-prefer-fstrings.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/AGENTS.md +121 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR001-strong-parameters.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR002-eager-load-includes.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR003-service-objects.md +99 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR004-active-job-background.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR005-pagination.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR006-find-each-batches.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR007-http-status-codes.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR008-before-action-auth.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR009-rails-credentials.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR010-scopes.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR011-counter-cache.md +59 -0
- package/skill-assets/sunlint-code-quality/rules/ruby-rails/RR012-render-json-status.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C006-verb-noun-functions.md +37 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C013-no-dead-code.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C014-dependency-injection.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C017-no-constructor-logic.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C018-generic-errors.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C019-error-log-level.md +64 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C020-no-unused-imports.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C022-no-unused-variables.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C023-no-duplicate-names.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C024-centralize-constants.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C029-catch-log-root-cause.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C030-custom-error-classes.md +77 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C033-separate-data-access.md +89 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C035-error-context-logging.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C041-no-hardcoded-secrets.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C042-boolean-naming.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C052-controller-parsing.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C060-superclass-logic.md +95 -0
- package/skill-assets/sunlint-code-quality/rules/swift/C067-no-hardcoded-config.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S003-sql-injection.md +65 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S004-no-log-credentials.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S005-server-authorization.md +73 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S006-default-credentials.md +76 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S007-output-encoding.md +96 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S009-approved-crypto.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S010-csprng.md +71 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S011-insecure-deserialization.md +74 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S012-secrets-management.md +81 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S013-tls-connections.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S017-parameterized-queries.md +86 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S019-session-management.md +131 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S020-kvc-injection.md +91 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S025-input-validation.md +125 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S029-brute-force-protection.md +120 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S036-path-traversal.md +102 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S039-tls-certificate-validation.md +109 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S041-logout-invalidation.md +103 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S043-password-hashing.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S044-critical-changes-reauth.md +145 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S045-debug-info-exposure.md +116 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S046-unvalidated-redirect.md +140 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S051-token-expiry.md +134 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S053-jwt-validation.md +139 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S059-background-snapshot-protection.md +113 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S060-data-protection-api.md +106 -0
- package/skill-assets/sunlint-code-quality/rules/swift/S061-jailbreak-detection.md +132 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN005 – Group Routes and Apply Middleware at Group Level"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Registering middleware per-route duplicates code and makes it easy to miss a route; group-level middleware is exhaustive by design."
|
|
5
|
+
tags: [go, gin, routing, middleware]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN005 – Group Routes and Apply Middleware at Group Level
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Use `router.Group()` to organise routes by prefix and protection level. Apply authentication, logging, and rate-limiting middleware to the group — not individual routes.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Per-route middleware registration (`r.GET("/users", authMiddleware, handler)`) requires the developer to remember to add middleware to every new route. A missed route is a security hole. Group-level middleware is applied to all current and future routes in the group automatically.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
r := gin.Default()
|
|
22
|
+
|
|
23
|
+
// ❌ Auth repeated on every route — easy to miss
|
|
24
|
+
r.GET("/users", authMiddleware, userHandler.List)
|
|
25
|
+
r.POST("/users", authMiddleware, userHandler.Create)
|
|
26
|
+
r.PUT("/users/:id", authMiddleware, userHandler.Update)
|
|
27
|
+
r.DELETE("/users/:id", authMiddleware, userHandler.Delete) // ❌ forgot auth on DELETE
|
|
28
|
+
|
|
29
|
+
// ❌ Mixed public and protected routes without groups — hard to audit
|
|
30
|
+
r.GET("/products", productHandler.List)
|
|
31
|
+
r.POST("/products", authMiddleware, adminMiddleware, productHandler.Create)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Correct
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
r := gin.New()
|
|
38
|
+
r.Use(gin.Recovery(), requestLogger()) // global middleware
|
|
39
|
+
|
|
40
|
+
// Public routes — no auth
|
|
41
|
+
public := r.Group("/api/v1")
|
|
42
|
+
{
|
|
43
|
+
public.POST("/auth/login", authHandler.Login)
|
|
44
|
+
public.POST("/auth/register", authHandler.Register)
|
|
45
|
+
public.GET("/products", productHandler.List)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Authenticated routes
|
|
49
|
+
authenticated := r.Group("/api/v1")
|
|
50
|
+
authenticated.Use(authMiddleware())
|
|
51
|
+
{
|
|
52
|
+
authenticated.GET("/users/me", userHandler.Profile)
|
|
53
|
+
authenticated.PUT("/users/me", userHandler.Update)
|
|
54
|
+
authenticated.GET("/orders", orderHandler.List)
|
|
55
|
+
authenticated.POST("/orders", orderHandler.Create)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Admin-only routes
|
|
59
|
+
admin := r.Group("/api/v1/admin")
|
|
60
|
+
admin.Use(authMiddleware(), adminMiddleware())
|
|
61
|
+
{
|
|
62
|
+
admin.GET("/users", adminUserHandler.List)
|
|
63
|
+
admin.DELETE("/users/:id", adminUserHandler.Delete)
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Notes
|
|
68
|
+
|
|
69
|
+
- Use curly braces `{}` inside `Group()` to make the scope visually clear (they're just Go blocks, not syntactically required).
|
|
70
|
+
- Apply rate limiting at the group level for public endpoints to prevent brute force on `/auth/login`.
|
|
71
|
+
- Version your API with a group: `r.Group("/api/v1")` makes future versioning (`/api/v2`) straightforward.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN006 – Return Correct HTTP Status Codes"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Returning 200 for all responses breaks API clients, hides errors in monitoring, and makes retry logic impossible."
|
|
5
|
+
tags: [go, gin, api, http]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN006 – Return Correct HTTP Status Codes
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Always pass the correct HTTP status code to `c.JSON()`, `c.AbortWithStatusJSON()`, and `c.Status()`. Never return `200 OK` for errors, resource creation, or empty responses.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
HTTP semantics are the contract between your API and its clients. A `200` with `{"error": "not found"}` is invisible to load balancers, monitoring tools, and client SDKs. Correct status codes enable automatic retry logic, circuit breakers, and accurate error dashboards.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
22
|
+
// ...
|
|
23
|
+
user, err := h.service.CreateUser(ctx, req)
|
|
24
|
+
if err != nil {
|
|
25
|
+
c.JSON(200, gin.H{"error": err.Error()}) // ❌ success code + error body
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
c.JSON(200, user) // ❌ should be 201 for newly created resource
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
func (h *UserHandler) Delete(c *gin.Context) {
|
|
32
|
+
h.service.DeleteUser(ctx, id)
|
|
33
|
+
c.JSON(200, gin.H{"message": "deleted"}) // ❌ should be 204 with no body
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Correct
|
|
38
|
+
|
|
39
|
+
```go
|
|
40
|
+
import "net/http"
|
|
41
|
+
|
|
42
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
43
|
+
var req CreateUserRequest
|
|
44
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
45
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()}) // 400
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
49
|
+
if err != nil {
|
|
50
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "creation failed"}) // 500
|
|
51
|
+
return
|
|
52
|
+
}
|
|
53
|
+
c.JSON(http.StatusCreated, user) // 201 ✅
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
func (h *UserHandler) GetUser(c *gin.Context) {
|
|
57
|
+
user, err := h.service.FindUserByID(c.Request.Context(), c.Param("id"))
|
|
58
|
+
if errors.Is(err, ErrNotFound) {
|
|
59
|
+
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "user not found"}) // 404
|
|
60
|
+
return
|
|
61
|
+
}
|
|
62
|
+
c.JSON(http.StatusOK, user) // 200
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func (h *UserHandler) Delete(c *gin.Context) {
|
|
66
|
+
if err := h.service.DeleteUser(c.Request.Context(), c.Param("id")); err != nil {
|
|
67
|
+
c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "not found"}) // 404
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
c.Status(http.StatusNoContent) // 204, no body ✅
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Status code reference
|
|
75
|
+
|
|
76
|
+
| Scenario | Code | Constant |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| Read success | 200 | `http.StatusOK` |
|
|
79
|
+
| Create success | 201 | `http.StatusCreated` |
|
|
80
|
+
| Delete / no body | 204 | `http.StatusNoContent` |
|
|
81
|
+
| Validation failed | 400 | `http.StatusBadRequest` |
|
|
82
|
+
| Not authenticated | 401 | `http.StatusUnauthorized` |
|
|
83
|
+
| Not authorized | 403 | `http.StatusForbidden` |
|
|
84
|
+
| Not found | 404 | `http.StatusNotFound` |
|
|
85
|
+
| Business rule violation | 422 | `http.StatusUnprocessableEntity` |
|
|
86
|
+
| Rate limited | 429 | `http.StatusTooManyRequests` |
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- Use `net/http` constants — never magic numbers like `c.JSON(201, ...)`.
|
|
91
|
+
- Map service-layer sentinel errors (e.g. `ErrNotFound`, `ErrConflict`) to status codes in a central error-mapping function.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN007 – Set gin.ReleaseMode in Production"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "The default gin.DebugMode prints every registered route and request detail to stdout, leaking your API surface and increasing log noise in production."
|
|
5
|
+
tags: [go, gin, security, configuration]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN007 – Set `gin.ReleaseMode` in Production
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Set `gin.SetMode(gin.ReleaseMode)` before calling `gin.New()` or `gin.Default()` in production builds. Read the mode from an environment variable so it can differ between environments.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Debug mode outputs all routes on startup and logs detailed request info. In production this:
|
|
17
|
+
- Leaks your API surface to anyone with log access.
|
|
18
|
+
- Adds unnecessary CPU cost for string formatting.
|
|
19
|
+
- Makes it harder to find meaningful log entries.
|
|
20
|
+
|
|
21
|
+
## Wrong
|
|
22
|
+
|
|
23
|
+
```go
|
|
24
|
+
// main.go — mode not set, defaults to DebugMode
|
|
25
|
+
func main() {
|
|
26
|
+
r := gin.Default() // ❌ runs in debug mode, dumps all routes to stdout
|
|
27
|
+
r.GET("/internal/admin", adminHandler) // ❌ exposed in startup log
|
|
28
|
+
r.Run(":8080")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Hardcoded mode
|
|
32
|
+
gin.SetMode(gin.DebugMode) // ❌ always debug, regardless of environment
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Correct
|
|
36
|
+
|
|
37
|
+
```go
|
|
38
|
+
func main() {
|
|
39
|
+
// Read from environment — "release", "debug", or "test"
|
|
40
|
+
ginMode := os.Getenv("GIN_MODE")
|
|
41
|
+
if ginMode == "" {
|
|
42
|
+
ginMode = gin.ReleaseMode // safe default
|
|
43
|
+
}
|
|
44
|
+
gin.SetMode(ginMode)
|
|
45
|
+
|
|
46
|
+
r := gin.New()
|
|
47
|
+
r.Use(gin.Recovery()) // see GN009 for Recovery middleware
|
|
48
|
+
// ... register routes
|
|
49
|
+
r.Run(":" + os.Getenv("PORT"))
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Or rely on Gin's built-in env var support: Gin automatically reads `GIN_MODE` — set it to `release` in your container/systemd environment and `gin.SetMode` is not even needed explicitly.
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Production container / systemd
|
|
57
|
+
GIN_MODE=release
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Notes
|
|
61
|
+
|
|
62
|
+
- `gin.Default()` adds `gin.Logger()` and `gin.Recovery()` automatically in debug mode — in production, use `gin.New()` and add your structured logger and Recovery explicitly (see GN009).
|
|
63
|
+
- Never call `gin.SetMode` after `gin.New()` — the mode must be set before the engine is created.
|
|
64
|
+
- In tests, set `gin.SetMode(gin.TestMode)` in `TestMain` to silence route debug output.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN008 – Use Struct Validation Binding Tags"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Manual if/else validation in handler code is incomplete, inconsistent, and untested; binding tags enforce validation at the deserialization boundary."
|
|
5
|
+
tags: [go, gin, validation, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN008 – Use Struct Validation Binding Tags
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Define all input validation rules using `binding:` struct tags on request structs. Do not write manual validation logic in handler functions. Handle binding errors as described in GN003.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Gin uses the `go-playground/validator` library under the hood for `ShouldBind*`. Struct tags declare constraints once, close to the field, and are validated automatically when binding. Scattered `if req.Age < 18` checks in handlers are often incomplete and inconsistent.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
type CreateUserRequest struct {
|
|
22
|
+
Name string `json:"name"`
|
|
23
|
+
Email string `json:"email"`
|
|
24
|
+
Age int `json:"age"`
|
|
25
|
+
Role string `json:"role"`
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Handler with manual validation — verbose, incomplete, inconsistent
|
|
29
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
30
|
+
var req CreateUserRequest
|
|
31
|
+
c.ShouldBindJSON(&req)
|
|
32
|
+
|
|
33
|
+
if req.Name == "" {
|
|
34
|
+
c.JSON(400, gin.H{"error": "name required"}) // ❌ still runs if bind errored
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
if !strings.Contains(req.Email, "@") { // ❌ incomplete email check
|
|
38
|
+
c.JSON(400, gin.H{"error": "invalid email"})
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
// ❌ forgot to validate Age and Role
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Correct
|
|
46
|
+
|
|
47
|
+
```go
|
|
48
|
+
type CreateUserRequest struct {
|
|
49
|
+
Name string `json:"name" binding:"required,min=2,max=100"`
|
|
50
|
+
Email string `json:"email" binding:"required,email"`
|
|
51
|
+
Age int `json:"age" binding:"required,min=18,max=120"`
|
|
52
|
+
Role string `json:"role" binding:"required,oneof=user admin moderator"`
|
|
53
|
+
Phone string `json:"phone" binding:"omitempty,e164"` // optional but validated if present
|
|
54
|
+
Website string `json:"website" binding:"omitempty,url"`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (h *UserHandler) Create(c *gin.Context) {
|
|
58
|
+
var req CreateUserRequest
|
|
59
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
60
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{
|
|
61
|
+
"error": "validation failed",
|
|
62
|
+
"details": formatValidationErrors(err), // see note below
|
|
63
|
+
})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// req is fully validated here — safe to use
|
|
67
|
+
user, err := h.service.CreateUser(c.Request.Context(), req)
|
|
68
|
+
// ...
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Common binding tags
|
|
73
|
+
|
|
74
|
+
| Tag | Meaning |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `required` | Field must be present and non-zero |
|
|
77
|
+
| `omitempty` | Skip validation if field is absent/zero |
|
|
78
|
+
| `min=N,max=M` | Min/max length for strings, min/max value for numbers |
|
|
79
|
+
| `email` | Valid email address format |
|
|
80
|
+
| `url` | Valid URL |
|
|
81
|
+
| `oneof=a b c` | Value must be one of the listed options |
|
|
82
|
+
| `uuid4` | Valid UUID v4 |
|
|
83
|
+
| `e164` | Valid international phone number |
|
|
84
|
+
| `gt=0` | Greater than 0 (useful for IDs) |
|
|
85
|
+
|
|
86
|
+
## Notes
|
|
87
|
+
|
|
88
|
+
- To return human-readable errors, type-assert `err` to `validator.ValidationErrors` and format field names.
|
|
89
|
+
- Custom validators: `binding.Validator.RegisterValidation("custom_rule", myFunc)`.
|
|
90
|
+
- For query parameters: `c.ShouldBindQuery(&req)` with `form:"field" binding:"required"` tags.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN009 – Add gin.Recovery() Middleware in Production"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Without Recovery(), an unhandled panic terminates the goroutine and returns a broken TCP connection or empty response to the client; with it, the server stays up and returns 500."
|
|
5
|
+
tags: [go, gin, reliability, middleware]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN009 – Add `gin.Recovery()` Middleware in Production
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Always include `gin.Recovery()` (or a custom recovery middleware) in the production middleware stack. Use `gin.New()` rather than `gin.Default()` and add middleware explicitly so you control what runs.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Go panics propagate up the call stack and terminate the goroutine. In a Gin HTTP handler, an unrecovered panic kills the request-handling goroutine and the server returns a broken connection. `gin.Recovery()` catches the panic, logs a stack trace, and returns `500 Internal Server Error`, keeping the server alive.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func main() {
|
|
22
|
+
r := gin.New() // ❌ no Recovery — a panic crashes the goroutine silently
|
|
23
|
+
r.GET("/users/:id", userHandler.Get)
|
|
24
|
+
r.Run(":8080")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// gin.Default() adds Logger + Recovery but also adds noise to structured log setups
|
|
28
|
+
r := gin.Default() // acceptable but not recommended for production structured logging
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Correct
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
func main() {
|
|
35
|
+
gin.SetMode(os.Getenv("GIN_MODE")) // see GN007
|
|
36
|
+
|
|
37
|
+
r := gin.New()
|
|
38
|
+
|
|
39
|
+
// Custom structured logger (replace gin.Logger())
|
|
40
|
+
r.Use(RequestLogger(logger))
|
|
41
|
+
|
|
42
|
+
// Recovery — MUST be registered before route handlers
|
|
43
|
+
r.Use(gin.RecoveryWithWriter(gin.DefaultErrorWriter))
|
|
44
|
+
// OR a custom recovery that logs stack traces to your logger:
|
|
45
|
+
r.Use(CustomRecovery(logger))
|
|
46
|
+
|
|
47
|
+
registerRoutes(r)
|
|
48
|
+
r.Run(":" + cfg.Port)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Custom recovery middleware with structured logging
|
|
52
|
+
func CustomRecovery(logger *slog.Logger) gin.HandlerFunc {
|
|
53
|
+
return gin.CustomRecoveryWithWriter(gin.DefaultErrorWriter, func(c *gin.Context, recovered any) {
|
|
54
|
+
logger.Error("panic recovered",
|
|
55
|
+
slog.Any("error", recovered),
|
|
56
|
+
slog.String("path", c.Request.URL.Path),
|
|
57
|
+
slog.String("method", c.Request.Method),
|
|
58
|
+
)
|
|
59
|
+
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Notes
|
|
65
|
+
|
|
66
|
+
- Recovery middleware must be registered **before** route handlers — Gin runs middleware in registration order.
|
|
67
|
+
- Don't expose the panic value or stack trace in the HTTP response — log it server-side only.
|
|
68
|
+
- For observability, send panic events to your error tracker (Sentry, Rollbar) inside the custom recovery handler.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN010 – Do Not Store gin.Context in Goroutines Beyond the Handler"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "gin.Context is recycled after the handler returns; storing it in a goroutine creates a data race on pool-recycled memory."
|
|
5
|
+
tags: [go, gin, concurrency, correctness]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN010 – Do Not Store `gin.Context` in Goroutines Beyond the Handler
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Never pass `*gin.Context` to a goroutine that outlives the handler function. Copy the values you need (request data, context) before spawning the goroutine.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Gin uses `sync.Pool` to reuse `*gin.Context` instances for performance. Once the handler function returns, Gin resets and recycles the context object. A goroutine holding a reference to the old `*gin.Context` now reads from a recycled object used by a completely different request — this is an undetected data race and a source of subtle, hard-to-reproduce bugs.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
func (h *NotificationHandler) Send(c *gin.Context) {
|
|
22
|
+
var req SendNotificationRequest
|
|
23
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
24
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// ❌ goroutine holds *gin.Context — races after handler returns
|
|
29
|
+
go func() {
|
|
30
|
+
userID := c.GetString("user_id") // c is already recycled here
|
|
31
|
+
h.service.Notify(context.Background(), userID, req)
|
|
32
|
+
}()
|
|
33
|
+
|
|
34
|
+
c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Correct
|
|
39
|
+
|
|
40
|
+
```go
|
|
41
|
+
func (h *NotificationHandler) Send(c *gin.Context) {
|
|
42
|
+
var req SendNotificationRequest
|
|
43
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
44
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
45
|
+
return
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ✅ Copy values from context BEFORE spawning the goroutine
|
|
49
|
+
userID := c.GetString("user_id") // copy string value
|
|
50
|
+
ctx := c.Request.Context() // copy the stdlib context (safe to pass to goroutine)
|
|
51
|
+
|
|
52
|
+
go func() {
|
|
53
|
+
// Use copied values — no reference to *gin.Context
|
|
54
|
+
if err := h.service.Notify(ctx, userID, req); err != nil {
|
|
55
|
+
h.logger.Error("notification failed", slog.String("user_id", userID), slog.Any("error", err))
|
|
56
|
+
}
|
|
57
|
+
}()
|
|
58
|
+
|
|
59
|
+
c.JSON(http.StatusAccepted, gin.H{"status": "queued"})
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Notes
|
|
64
|
+
|
|
65
|
+
- `c.Request.Context()` is a standard `context.Context` backed by the HTTP request — safe to copy and pass to goroutines.
|
|
66
|
+
- Extract all required values (`c.Param()`, `c.GetHeader()`, `c.Get()`) into local variables before the goroutine.
|
|
67
|
+
- The Go race detector (`go test -race`) will catch this violation in tests if the goroutine runs fast enough — enable it in CI.
|
|
68
|
+
- Prefer `c.Copy()` if you absolutely need a snapshot of the whole context, but this is memory-heavier than copying specific fields.
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN011 – Apply Cross-Cutting Concerns via Middleware"
|
|
3
|
+
impact: medium
|
|
4
|
+
impactDescription: "Duplicating auth checks, rate limiting, or logging inside individual handlers makes enforcement inconsistent and maintenance expensive."
|
|
5
|
+
tags: [go, gin, middleware, architecture]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN011 – Apply Cross-Cutting Concerns via Middleware
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Authentication, authorization, rate limiting, request logging, request ID injection, and CORS must be implemented as Gin middleware functions and applied at the router/group level — not duplicated inside individual handlers.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
When authentication logic lives inside each handler, a single missed handler creates a security gap. Middleware ensures uniform enforcement. It also separates concerns: handlers contain only business logic; infrastructure concerns live in middleware.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
// ❌ Auth duplicated in every handler
|
|
22
|
+
func (h *OrderHandler) List(c *gin.Context) {
|
|
23
|
+
token := c.GetHeader("Authorization")
|
|
24
|
+
userID, err := h.auth.ValidateToken(token)
|
|
25
|
+
if err != nil {
|
|
26
|
+
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
// ... business logic
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func (h *OrderHandler) Create(c *gin.Context) {
|
|
33
|
+
token := c.GetHeader("Authorization") // ❌ duplicated
|
|
34
|
+
userID, err := h.auth.ValidateToken(token)
|
|
35
|
+
// ...
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Correct
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
// middleware/auth.go
|
|
43
|
+
func JWTAuth(tokenValidator TokenValidator) gin.HandlerFunc {
|
|
44
|
+
return func(c *gin.Context) {
|
|
45
|
+
token := strings.TrimPrefix(c.GetHeader("Authorization"), "Bearer ")
|
|
46
|
+
claims, err := tokenValidator.Validate(token)
|
|
47
|
+
if err != nil {
|
|
48
|
+
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
c.Set("user_id", claims.UserID)
|
|
52
|
+
c.Set("user_role", claims.Role)
|
|
53
|
+
c.Next()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// middleware/request_id.go
|
|
58
|
+
func RequestID() gin.HandlerFunc {
|
|
59
|
+
return func(c *gin.Context) {
|
|
60
|
+
id := c.GetHeader("X-Request-ID")
|
|
61
|
+
if id == "" {
|
|
62
|
+
id = uuid.New().String()
|
|
63
|
+
}
|
|
64
|
+
c.Set("request_id", id)
|
|
65
|
+
c.Header("X-Request-ID", id)
|
|
66
|
+
c.Next()
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// main.go — applied at group level (see GN005)
|
|
71
|
+
r := gin.New()
|
|
72
|
+
r.Use(gin.Recovery(), RequestID(), StructuredLogger(logger))
|
|
73
|
+
|
|
74
|
+
api := r.Group("/api/v1")
|
|
75
|
+
api.Use(JWTAuth(tokenValidator))
|
|
76
|
+
api.Use(RateLimit(100, time.Minute))
|
|
77
|
+
|
|
78
|
+
api.GET("/orders", orderHandler.List) // auth + rate limit automatically applied
|
|
79
|
+
api.POST("/orders", orderHandler.Create)
|
|
80
|
+
|
|
81
|
+
// handlers are clean — no auth logic
|
|
82
|
+
func (h *OrderHandler) List(c *gin.Context) {
|
|
83
|
+
userID := c.GetString("user_id") // set by middleware
|
|
84
|
+
// ... just business logic
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Notes
|
|
89
|
+
|
|
90
|
+
- Middleware should only set values in the context and call `c.Next()` or `c.Abort()` — no response writing for non-auth middleware.
|
|
91
|
+
- Return factory functions (`func JWTAuth(dep Dep) gin.HandlerFunc`) so middleware is injectable and testable.
|
|
92
|
+
- Always document what a middleware sets in the context (e.g. `"user_id"`, `"request_id"`) so handlers know what keys are available.
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "GN012 – Do Not Log Request Bodies Containing Sensitive Data"
|
|
3
|
+
impact: high
|
|
4
|
+
impactDescription: "Logging raw request bodies exposes passwords, tokens, credit card numbers, and PII to anyone with log access, violating GDPR, PCI-DSS, and basic security hygiene."
|
|
5
|
+
tags: [go, gin, security, logging]
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# GN012 – Do Not Log Request Bodies Containing Sensitive Data
|
|
9
|
+
|
|
10
|
+
## Rule
|
|
11
|
+
|
|
12
|
+
Never log the raw HTTP request body. Log only request metadata (method, path, status, duration, request ID). If you must log body fields for debugging, explicitly allowlist safe fields and mask or omit sensitive ones.
|
|
13
|
+
|
|
14
|
+
## Why
|
|
15
|
+
|
|
16
|
+
Request bodies for auth endpoints contain passwords. Payment endpoints contain card data. Profile endpoints contain PII. Logging bodies stores this data in plaintext log files, log aggregators, and monitoring tools — where it lives far beyond the request lifetime and is often accessible to many people.
|
|
17
|
+
|
|
18
|
+
## Wrong
|
|
19
|
+
|
|
20
|
+
```go
|
|
21
|
+
// ❌ Logging the full request body
|
|
22
|
+
func RequestBodyLogger() gin.HandlerFunc {
|
|
23
|
+
return func(c *gin.Context) {
|
|
24
|
+
body, _ := io.ReadAll(c.Request.Body)
|
|
25
|
+
log.Printf("Request body: %s", body) // ❌ logs passwords, tokens, PII
|
|
26
|
+
c.Request.Body = io.NopCloser(bytes.NewBuffer(body))
|
|
27
|
+
c.Next()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ❌ Logging bound request struct directly (may contain passwords)
|
|
32
|
+
func (h *AuthHandler) Login(c *gin.Context) {
|
|
33
|
+
var req LoginRequest
|
|
34
|
+
c.ShouldBindJSON(&req)
|
|
35
|
+
log.Printf("Login attempt: %+v", req) // ❌ req.Password logged in plaintext
|
|
36
|
+
}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Correct
|
|
40
|
+
|
|
41
|
+
```go
|
|
42
|
+
// ✅ Log only safe metadata — never raw body
|
|
43
|
+
func RequestLogger(logger *slog.Logger) gin.HandlerFunc {
|
|
44
|
+
return func(c *gin.Context) {
|
|
45
|
+
start := time.Now()
|
|
46
|
+
c.Next()
|
|
47
|
+
logger.Info("request",
|
|
48
|
+
slog.String("method", c.Request.Method),
|
|
49
|
+
slog.String("path", c.Request.URL.Path),
|
|
50
|
+
slog.Int("status", c.Writer.Status()),
|
|
51
|
+
slog.Duration("duration", time.Since(start)),
|
|
52
|
+
slog.String("ip", c.ClientIP()),
|
|
53
|
+
slog.String("request_id", c.GetString("request_id")),
|
|
54
|
+
// ✅ no body, no headers that might contain auth tokens
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ✅ Log only safe fields from struct - never password/token fields
|
|
60
|
+
func (h *AuthHandler) Login(c *gin.Context) {
|
|
61
|
+
var req LoginRequest
|
|
62
|
+
if err := c.ShouldBindJSON(&req); err != nil {
|
|
63
|
+
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
// ✅ Log only the email, not the password
|
|
67
|
+
h.logger.Info("login attempt", slog.String("email", req.Email))
|
|
68
|
+
// ...
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Sensitive fields to never log
|
|
73
|
+
|
|
74
|
+
- `password`, `password_confirmation`, `current_password`
|
|
75
|
+
- `token`, `access_token`, `refresh_token`, `api_key`, `secret`
|
|
76
|
+
- `card_number`, `cvv`, `expiry`
|
|
77
|
+
- `ssn`, `national_id`, `date_of_birth` (PII)
|
|
78
|
+
- `Authorization` header value
|
|
79
|
+
|
|
80
|
+
## Notes
|
|
81
|
+
|
|
82
|
+
- If you need request body logging for debugging, create a separate debug middleware that is only enabled when `GIN_MODE=debug`.
|
|
83
|
+
- Use log scrubbing libraries (e.g., `go-sanitize`) as a last-resort safety net, but don't rely on them — fix the root cause.
|
|
84
|
+
- `c.Request.Header` can contain `Authorization: Bearer <token>` — log header names only, never values.
|