@sun-asterisk/sunlint 1.3.40 → 1.3.41
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/rule-selection-service.js +11 -0
- package/package.json +1 -1
- package/skill-assets/sunlint-code-quality/rules/go/C006-verb-noun-functions.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/C013-no-dead-code.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/C014-dependency-injection.md +85 -0
- package/skill-assets/sunlint-code-quality/rules/go/C017-no-constructor-logic.md +67 -0
- package/skill-assets/sunlint-code-quality/rules/go/C018-generic-errors.md +63 -0
- package/skill-assets/sunlint-code-quality/rules/go/C019-error-log-level.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/go/C020-no-unused-imports.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/C022-no-unused-variables.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/go/C023-no-duplicate-names.md +41 -0
- package/skill-assets/sunlint-code-quality/rules/go/C024-centralize-constants.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/go/C029-catch-log-root-cause.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/go/C030-custom-error-classes.md +69 -0
- package/skill-assets/sunlint-code-quality/rules/go/C033-separate-data-access.md +68 -0
- package/skill-assets/sunlint-code-quality/rules/go/C035-error-context-logging.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/C041-no-hardcoded-secrets.md +45 -0
- package/skill-assets/sunlint-code-quality/rules/go/C042-boolean-naming.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/go/C052-controller-parsing.md +62 -0
- package/skill-assets/sunlint-code-quality/rules/go/C060-superclass-logic.md +60 -0
- package/skill-assets/sunlint-code-quality/rules/go/C067-no-hardcoded-config.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/go/S003-open-redirect.md +80 -0
- package/skill-assets/sunlint-code-quality/rules/go/S004-no-log-credentials.md +66 -0
- package/skill-assets/sunlint-code-quality/rules/go/S005-server-authorization.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/go/S006-default-credentials.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/go/S007-output-encoding.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/go/S009-approved-crypto.md +63 -0
- package/skill-assets/sunlint-code-quality/rules/go/S010-csprng.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/S011-encrypted-client-hello.md +34 -0
- package/skill-assets/sunlint-code-quality/rules/go/S012-secrets-management.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/S013-tls-connections.md +61 -0
- package/skill-assets/sunlint-code-quality/rules/go/S016-no-sensitive-query-string.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/go/S017-parameterized-queries.md +36 -0
- package/skill-assets/sunlint-code-quality/rules/go/S019-email-input-sanitization.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/S020-eval-code-execution.md +47 -0
- package/skill-assets/sunlint-code-quality/rules/go/S022-context-escaping.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/S023-dynamic-js-encoding.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/go/S025-server-validation.md +57 -0
- package/skill-assets/sunlint-code-quality/rules/go/S026-tls-encryption.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/go/S027-mtls-validation.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/go/S028-upload-limits.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/go/S029-csrf-protection.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/S030-directory-browsing.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/S031-secure-cookie-flag.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/S032-httponly-cookie.md +42 -0
- package/skill-assets/sunlint-code-quality/rules/go/S033-samesite-cookie.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/S034-host-prefix-cookie.md +44 -0
- package/skill-assets/sunlint-code-quality/rules/go/S035-app-hostnames.md +50 -0
- package/skill-assets/sunlint-code-quality/rules/go/S036-internal-file-paths.md +56 -0
- package/skill-assets/sunlint-code-quality/rules/go/S037-anti-cache-headers.md +43 -0
- package/skill-assets/sunlint-code-quality/rules/go/S039-tls-certificate-validation.md +41 -0
- package/skill-assets/sunlint-code-quality/rules/go/S041-logout-invalidation.md +46 -0
- package/skill-assets/sunlint-code-quality/rules/go/S042-long-lived-sessions.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/go/S044-critical-changes-reauth.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/S045-brute-force-protection.md +55 -0
- package/skill-assets/sunlint-code-quality/rules/go/S047-oauth-csrf-protection.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/go/S048-oauth-redirect-validation.md +58 -0
- package/skill-assets/sunlint-code-quality/rules/go/S049-auth-code-expiry.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/go/S050-token-entropy.md +53 -0
- package/skill-assets/sunlint-code-quality/rules/go/S051-password-length.md +49 -0
- package/skill-assets/sunlint-code-quality/rules/go/S052-otp-entropy.md +48 -0
- package/skill-assets/sunlint-code-quality/rules/go/S053-generic-error-messages.md +51 -0
- package/skill-assets/sunlint-code-quality/rules/go/S054-no-default-admin.md +43 -0
- package/skill-assets/sunlint-code-quality/rules/go/S055-content-type-validation.md +52 -0
- package/skill-assets/sunlint-code-quality/rules/go/S056-log-injection.md +40 -0
- package/skill-assets/sunlint-code-quality/rules/go/S057-synchronized-time.md +40 -0
- package/skill-assets/sunlint-code-quality/rules/go/S058-ssrf-protection.md +70 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Separate Processing And Data Access
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables testable business logic
|
|
5
|
+
tags: separation, repository, service, architecture, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Separate Processing And Data Access
|
|
9
|
+
|
|
10
|
+
Mixing business logic with database queries creates tight coupling and makes testing require real databases.
|
|
11
|
+
|
|
12
|
+
**Incorrect (mixed concerns):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
type OrderService struct {
|
|
16
|
+
db *sql.DB
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
func (s *OrderService) CalculateDiscount(userId string) (int, error) {
|
|
20
|
+
// Business logic mixed with data access
|
|
21
|
+
var isPremium bool
|
|
22
|
+
db.QueryRow("SELECT is_premium FROM users WHERE id = ?", userId).Scan(&isPremium)
|
|
23
|
+
|
|
24
|
+
var orderCount int
|
|
25
|
+
db.QueryRow("SELECT count(*) FROM orders WHERE user_id = ?", userId).Scan(&orderCount)
|
|
26
|
+
|
|
27
|
+
discount := 0
|
|
28
|
+
if orderCount > 10 { discount += 5 }
|
|
29
|
+
if isPremium { discount += 10 }
|
|
30
|
+
|
|
31
|
+
return discount, nil
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Correct (separated layers):**
|
|
36
|
+
|
|
37
|
+
```go
|
|
38
|
+
// Repository - data access only
|
|
39
|
+
type UserRepository interface {
|
|
40
|
+
FindByID(id string) (*User, error)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type OrderRepository interface {
|
|
44
|
+
CountByUserID(id string) (int, error)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Service - business logic only
|
|
48
|
+
type DiscountService struct {
|
|
49
|
+
userRepo UserRepository
|
|
50
|
+
orderRepo OrderRepository
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
func (s *DiscountService) CalculateDiscount(userId string) (int, error) {
|
|
54
|
+
user, _ := s.userRepo.FindByID(userId)
|
|
55
|
+
count, _ := s.orderRepo.CountByUserID(userId)
|
|
56
|
+
|
|
57
|
+
return s.computeDiscount(user, count), nil
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
func (s *DiscountService) computeDiscount(user *User, orderCount int) int {
|
|
61
|
+
discount := 0
|
|
62
|
+
if orderCount > 10 { discount += 5 }
|
|
63
|
+
if user != nil && user.IsPremium { discount += 10 }
|
|
64
|
+
return discount
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Tools:** Architectural review, Code Review
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Log All Relevant Context On Errors
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables quick debugging and incident response
|
|
5
|
+
tags: error-handling, logging, context, debugging, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Log All Relevant Context On Errors
|
|
9
|
+
|
|
10
|
+
Context-rich logs enable quick debugging. Without proper context, finding root causes is difficult.
|
|
11
|
+
|
|
12
|
+
**Incorrect (minimal context):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
slog.Error("error occurred")
|
|
16
|
+
slog.Error(err.Error())
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
**Correct (comprehensive context using slog):**
|
|
20
|
+
|
|
21
|
+
```go
|
|
22
|
+
slog.Error("failed to process order",
|
|
23
|
+
// What happened
|
|
24
|
+
"error", err,
|
|
25
|
+
"stack", string(debug.Stack()), // Optional: but useful for critical errors
|
|
26
|
+
|
|
27
|
+
// Context
|
|
28
|
+
"order_id", order.ID,
|
|
29
|
+
"user_id", user.ID,
|
|
30
|
+
"request_id", ctx.Value("request_id"),
|
|
31
|
+
|
|
32
|
+
// Input that caused the issue
|
|
33
|
+
"item_count", len(order.Items),
|
|
34
|
+
"total_amount", order.Total,
|
|
35
|
+
|
|
36
|
+
// Timing
|
|
37
|
+
"processing_time_ms", time.Since(startTime).Milliseconds(),
|
|
38
|
+
)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Essential context:**
|
|
42
|
+
- Error details
|
|
43
|
+
- Entity identifiers
|
|
44
|
+
- Request/correlation IDs
|
|
45
|
+
- Relevant input summary
|
|
46
|
+
- Timing information
|
|
47
|
+
|
|
48
|
+
**Tools:** `slog`, `zap`, `logback` equivalent
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: No Hardcoded Secrets In Repo
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents credential exposure
|
|
5
|
+
tags: secrets, credentials, security, git, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## No Hardcoded Secrets In Repo
|
|
9
|
+
|
|
10
|
+
Secrets in code are exposed to everyone with repo access and can be scraped by attackers.
|
|
11
|
+
|
|
12
|
+
**Incorrect (secrets in code):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
const APIKey = "sk-abc123xyz789"
|
|
16
|
+
const DBPassword = "admin123"
|
|
17
|
+
|
|
18
|
+
func init() {
|
|
19
|
+
client := stripe.NewClient("sk_live_xxx")
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Correct (environment/secrets manager):**
|
|
24
|
+
|
|
25
|
+
```go
|
|
26
|
+
// From environment
|
|
27
|
+
apiKey := os.Getenv("API_KEY")
|
|
28
|
+
|
|
29
|
+
// From secrets manager
|
|
30
|
+
apiKey, err := secretManager.GetSecret(ctx, "stripe-api-key")
|
|
31
|
+
|
|
32
|
+
// Validation at startup
|
|
33
|
+
if os.Getenv("API_KEY") == "" {
|
|
34
|
+
log.Fatal("API_KEY environment variable is required")
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```gitignore
|
|
39
|
+
# .gitignore
|
|
40
|
+
.env
|
|
41
|
+
*.pem
|
|
42
|
+
*.key
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Tools:** GitLeaks, TruffleHog, pre-commit hooks
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Boolean Names Is/Has/Should
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: makes conditions instantly readable
|
|
5
|
+
tags: naming, booleans, readability, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Boolean Names Is/Has/Should
|
|
9
|
+
|
|
10
|
+
Boolean prefixes make conditions instantly readable.
|
|
11
|
+
|
|
12
|
+
**Incorrect (unclear boolean names):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
active := user.Status == "active"
|
|
16
|
+
admin := checkAdminRole(user)
|
|
17
|
+
items := len(cart) > 0
|
|
18
|
+
update := needsRefresh()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Correct (boolean prefixes):**
|
|
22
|
+
|
|
23
|
+
```go
|
|
24
|
+
isActive := user.Status == "active"
|
|
25
|
+
isAdmin := checkAdminRole(user)
|
|
26
|
+
hasItems := len(cart) > 0
|
|
27
|
+
shouldUpdate := needsRefresh()
|
|
28
|
+
canEdit := hasPermission(user, "edit")
|
|
29
|
+
willExpire := expirationDate.Before(time.Now())
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Boolean prefixes:**
|
|
33
|
+
|
|
34
|
+
| Prefix | Use For |
|
|
35
|
+
|--------|---------|
|
|
36
|
+
| `Is` | State (IsActive, IsEnabled) |
|
|
37
|
+
| `Has` | Ownership (HasPermission, HasError) |
|
|
38
|
+
| `Should` | Decision (ShouldUpdate, ShouldRetry) |
|
|
39
|
+
| `Can` | Capability (CanEdit, CanDelete) |
|
|
40
|
+
| `Will` | Future (WillExpire, WillRetry) |
|
|
41
|
+
|
|
42
|
+
**Tools:** Linter, Code Review
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Separate Parsing From Handlers
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: keeps handlers thin and focused
|
|
5
|
+
tags: handler, parsing, transformation, patterns, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Separate Parsing From Handlers
|
|
9
|
+
|
|
10
|
+
Handlers (controllers) should be thin - only handling HTTP concerns. Transformation logic should be extracted.
|
|
11
|
+
|
|
12
|
+
**Incorrect (transformation in handler):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
16
|
+
user, _ := userService.FindByID(r.URL.Query().Get("id"))
|
|
17
|
+
|
|
18
|
+
// Transformation logic in handler
|
|
19
|
+
response := map[string]any{
|
|
20
|
+
"id": user.ID,
|
|
21
|
+
"full_name": user.FirstName + " " + user.LastName,
|
|
22
|
+
"email": strings.ToLower(user.Email),
|
|
23
|
+
"created_at": user.CreatedAt.Format("2006-01-02"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
json.NewEncoder(w).Encode(response)
|
|
27
|
+
}
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct (separate mapper/DTO):**
|
|
31
|
+
|
|
32
|
+
```go
|
|
33
|
+
type UserResponse struct {
|
|
34
|
+
ID string `json:"id"`
|
|
35
|
+
FullName string `json:"full_name"`
|
|
36
|
+
Email string `json:"email"`
|
|
37
|
+
CreatedAt string `json:"created_at"`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func ToUserResponse(user *User) UserResponse {
|
|
41
|
+
return UserResponse{
|
|
42
|
+
ID: user.ID,
|
|
43
|
+
FullName: user.FirstName + " " + user.LastName,
|
|
44
|
+
Email: strings.ToLower(user.Email),
|
|
45
|
+
CreatedAt: user.CreatedAt.Format("2006-01-02"),
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Clean handler
|
|
50
|
+
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
51
|
+
user, _ := userService.FindByID(r.URL.Query().Get("id"))
|
|
52
|
+
json.NewEncoder(w).Encode(ToUserResponse(user))
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
**Benefits:**
|
|
57
|
+
- Reusable transformation logic
|
|
58
|
+
- Testable mappers
|
|
59
|
+
- Clean handlers
|
|
60
|
+
- Consistent response format
|
|
61
|
+
|
|
62
|
+
**Tools:** Code review, Architecture rules
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Ignore Embedded Struct Logic
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: ensures proper composition behavior
|
|
5
|
+
tags: composition, embedding, override, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Ignore Embedded Struct Logic
|
|
9
|
+
|
|
10
|
+
When "overriding" methods of an embedded struct, ensure the embedded behavior is preserved unless explicitly intended otherwise.
|
|
11
|
+
|
|
12
|
+
**Incorrect (ignoring embedded logic):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
type BaseService struct{}
|
|
16
|
+
|
|
17
|
+
func (s *BaseService) Save(entity any) error {
|
|
18
|
+
s.Validate(entity)
|
|
19
|
+
s.BeforeSave(entity)
|
|
20
|
+
// ... actual save logic ...
|
|
21
|
+
return nil
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type UserService struct {
|
|
25
|
+
BaseService
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
func (s *UserService) Save(user any) error {
|
|
29
|
+
// Completely ignores BaseService.Save logic (validation, hooks, etc.)
|
|
30
|
+
return s.repo.Save(user)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Correct (explicitly calling embedded method):**
|
|
35
|
+
|
|
36
|
+
```go
|
|
37
|
+
type UserService struct {
|
|
38
|
+
BaseService
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
func (s *UserService) Save(user *User) error {
|
|
42
|
+
// Add user-specific preprocessing
|
|
43
|
+
user.UpdatedAt = time.Now()
|
|
44
|
+
|
|
45
|
+
// Call embedded struct implementation
|
|
46
|
+
if err := s.BaseService.Save(user); err != nil {
|
|
47
|
+
return err
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Add user-specific postprocessing
|
|
51
|
+
return s.updateSearchIndex(user)
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**When to skip:**
|
|
56
|
+
- Complete replacement is intentional
|
|
57
|
+
- Base implementation doesn't apply
|
|
58
|
+
- Document the reason clearly
|
|
59
|
+
|
|
60
|
+
**Tools:** Static Analysis, Code Review
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Hardcode Configuration
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: enables environment-specific deployments
|
|
5
|
+
tags: configuration, environment, deployment, quality
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Hardcode Configuration
|
|
9
|
+
|
|
10
|
+
Hardcoded configuration requires code changes to deploy to different environments.
|
|
11
|
+
|
|
12
|
+
**Incorrect (hardcoded config):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
const APIUrl = "https://api.production.example.com"
|
|
16
|
+
const Timeout = 5000
|
|
17
|
+
const MaxFileSize = 10485760
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Correct (externalized config):**
|
|
21
|
+
|
|
22
|
+
```go
|
|
23
|
+
type Config struct {
|
|
24
|
+
API struct {
|
|
25
|
+
URL string
|
|
26
|
+
Timeout time.Duration
|
|
27
|
+
}
|
|
28
|
+
Upload struct {
|
|
29
|
+
MaxFileSize int64
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
func LoadConfig() *Config {
|
|
34
|
+
return &Config{
|
|
35
|
+
API: struct {
|
|
36
|
+
URL string
|
|
37
|
+
Timeout time.Duration
|
|
38
|
+
}{
|
|
39
|
+
URL: getEnv("API_URL", "http://localhost:3000"),
|
|
40
|
+
Timeout: time.Duration(getEnvInt("API_TIMEOUT", 5000)) * time.Millisecond,
|
|
41
|
+
},
|
|
42
|
+
// ...
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Usage
|
|
47
|
+
cfg := LoadConfig()
|
|
48
|
+
client := &http.Client{Timeout: cfg.API.Timeout}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
**Tools:** `os.Getenv`, `viper`, `clever-env`, Manual review
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: URL Redirects Must Be In Allow List
|
|
3
|
+
impact: LOW
|
|
4
|
+
impactDescription: prevents open redirect vulnerabilities
|
|
5
|
+
tags: redirect, url, allow-list, validation, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## URL Redirects Must Be In Allow List
|
|
9
|
+
|
|
10
|
+
Open redirect vulnerabilities allow attackers to redirect users to malicious sites, often used in phishing attacks.
|
|
11
|
+
|
|
12
|
+
**Incorrect (unvalidated redirect URL):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
// Open redirect vulnerability
|
|
16
|
+
http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
|
17
|
+
url := r.URL.Query().Get("url")
|
|
18
|
+
http.Redirect(w, r, url, http.StatusFound) // Attacker: ?url=https://evil.com
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
// Partial validation (can be bypassed)
|
|
22
|
+
http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
|
23
|
+
url := r.URL.Query().Get("url")
|
|
24
|
+
if strings.Contains(url, "example.com") {
|
|
25
|
+
http.Redirect(w, r, url, http.StatusFound) // Bypass: evil.com?example.com
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Correct (allow list validation):**
|
|
31
|
+
|
|
32
|
+
```go
|
|
33
|
+
var allowedRedirectHosts = []string{
|
|
34
|
+
"example.com",
|
|
35
|
+
"app.example.com",
|
|
36
|
+
"admin.example.com",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func isAllowedHost(host string) bool {
|
|
40
|
+
for _, h := range allowedRedirectHosts {
|
|
41
|
+
if h == host {
|
|
42
|
+
return true
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
http.HandleFunc("/redirect", func(w http.ResponseWriter, r *http.Request) {
|
|
49
|
+
targetURL := r.URL.Query().Get("url")
|
|
50
|
+
|
|
51
|
+
parsed, err := url.Parse(targetURL)
|
|
52
|
+
if err != nil || !isAllowedHost(parsed.Hostname()) {
|
|
53
|
+
http.Error(w, "Invalid redirect URL", http.StatusBadRequest)
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
http.Redirect(w, r, targetURL, http.StatusFound)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
// Or use relative URLs only
|
|
61
|
+
http.HandleFunc("/relative-redirect", func(w http.ResponseWriter, r *http.Request) {
|
|
62
|
+
path := r.URL.Query().Get("path")
|
|
63
|
+
|
|
64
|
+
// Only allow relative paths starting with /
|
|
65
|
+
if !strings.HasPrefix(path, "/") || strings.HasPrefix(path, "//") {
|
|
66
|
+
http.Error(w, "Invalid path", http.StatusBadRequest)
|
|
67
|
+
return
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
http.Redirect(w, r, path, http.StatusFound)
|
|
71
|
+
})
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Protection strategies:**
|
|
75
|
+
1. Allow list of trusted domains
|
|
76
|
+
2. Use relative URLs only
|
|
77
|
+
3. Validate URL structure
|
|
78
|
+
4. Warning page before external redirects
|
|
79
|
+
|
|
80
|
+
**Tools:** SonarQube, Semgrep, Manual Review
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Log Credentials Or Tokens
|
|
3
|
+
impact: MEDIUM
|
|
4
|
+
impactDescription: prevents credential exposure in logs
|
|
5
|
+
tags: logging, credentials, tokens, secrets, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Log Credentials Or Tokens
|
|
9
|
+
|
|
10
|
+
Logs are often stored unencrypted and accessed by many people. Credentials in logs can be harvested by attackers or accidentally exposed.
|
|
11
|
+
|
|
12
|
+
**Incorrect (logging sensitive data):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
// Logging passwords
|
|
16
|
+
slog.Info("Login attempt",
|
|
17
|
+
"username", user.Username,
|
|
18
|
+
"password", user.Password, // NEVER!
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// Logging tokens
|
|
22
|
+
slog.Debug("Request headers", "headers", r.Header)
|
|
23
|
+
// Authorization header contains token!
|
|
24
|
+
|
|
25
|
+
// Logging full request body
|
|
26
|
+
body, _ := io.ReadAll(r.Body)
|
|
27
|
+
slog.Info("Incoming request", "body", string(body))
|
|
28
|
+
// May contain password, credit card, etc.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Correct (sanitized logging):**
|
|
32
|
+
|
|
33
|
+
```go
|
|
34
|
+
// Mask or omit sensitive fields
|
|
35
|
+
slog.Info("Login attempt",
|
|
36
|
+
"username", user.Username,
|
|
37
|
+
// password omitted
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Sanitize headers
|
|
41
|
+
safeHeader := r.Header.Clone()
|
|
42
|
+
if safeHeader.Get("Authorization") != "" {
|
|
43
|
+
safeHeader.Set("Authorization", "[REDACTED]")
|
|
44
|
+
}
|
|
45
|
+
slog.Debug("Request headers", "headers", safeHeader)
|
|
46
|
+
|
|
47
|
+
// Use a sanitizer for request body
|
|
48
|
+
func sanitizeForLog(data map[string]any) map[string]any {
|
|
49
|
+
sensitiveFields := []string{"password", "token", "secret", "credit_card"}
|
|
50
|
+
for _, field := range sensitiveFields {
|
|
51
|
+
if _, ok := data[field]; ok {
|
|
52
|
+
data[field] = "[REDACTED]"
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return data
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Never log:**
|
|
60
|
+
- Passwords (plaintext or hashed)
|
|
61
|
+
- API keys and tokens
|
|
62
|
+
- Credit card numbers
|
|
63
|
+
- Social Security Numbers
|
|
64
|
+
- Session identifiers
|
|
65
|
+
|
|
66
|
+
**Tools:** SonarQube, Semgrep, Log Audit
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Enforce Authorization At Trusted Service Layer
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: prevents client-side authorization bypass
|
|
5
|
+
tags: authorization, server-side, middleware, access-control, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Enforce Authorization At Trusted Service Layer
|
|
9
|
+
|
|
10
|
+
Client-side authorization can be bypassed. All permission checks must occur server-side where they cannot be manipulated.
|
|
11
|
+
|
|
12
|
+
**Incorrect (client-side or trusting client data):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
// Trusting client-sent role
|
|
16
|
+
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
|
|
17
|
+
userRole := r.FormValue("role") // From client!
|
|
18
|
+
if userRole == "admin" {
|
|
19
|
+
deleteUser(r.FormValue("id"))
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct (server-side authorization middleware):**
|
|
25
|
+
|
|
26
|
+
```go
|
|
27
|
+
func authMiddleware(requiredRole string, next http.HandlerFunc) http.HandlerFunc {
|
|
28
|
+
return func(w http.ResponseWriter, r *http.Request) {
|
|
29
|
+
token := r.Header.Get("Authorization")
|
|
30
|
+
user, err := getUserFromToken(token)
|
|
31
|
+
if err != nil {
|
|
32
|
+
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if !checkPermission(user.ID, requiredRole) {
|
|
37
|
+
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
next.ServeHTTP(w, r)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Router usage
|
|
46
|
+
http.HandleFunc("/users/delete", authMiddleware("admin", deleteUserHandler))
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Never trust:**
|
|
50
|
+
- Client-side JavaScript checks
|
|
51
|
+
- Hidden form fields
|
|
52
|
+
- URL parameters for access control
|
|
53
|
+
- Unvalidated tokens from browser storage
|
|
54
|
+
|
|
55
|
+
**Tools:** Manual Review, Static Analysis, Penetration Testing
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Do Not Use Default Credentials
|
|
3
|
+
impact: CRITICAL
|
|
4
|
+
impactDescription: prevents trivial compromise via known credentials
|
|
5
|
+
tags: credentials, default, passwords, configuration, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Do Not Use Default Credentials
|
|
9
|
+
|
|
10
|
+
Default credentials are publicly known. Attackers scan for them automatically, making any system using them trivially compromised.
|
|
11
|
+
|
|
12
|
+
**Incorrect (default or hardcoded credentials):**
|
|
13
|
+
|
|
14
|
+
```yaml
|
|
15
|
+
# Docker Compose with defaults
|
|
16
|
+
services:
|
|
17
|
+
postgres:
|
|
18
|
+
image: postgres
|
|
19
|
+
environment:
|
|
20
|
+
POSTGRES_USER: postgres
|
|
21
|
+
POSTGRES_PASSWORD: postgres # Default!
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
**Correct (environment/secrets management):**
|
|
25
|
+
|
|
26
|
+
```go
|
|
27
|
+
// Application code
|
|
28
|
+
dbConfig := struct {
|
|
29
|
+
User string
|
|
30
|
+
Password string
|
|
31
|
+
}{
|
|
32
|
+
User: os.Getenv("DB_USER"),
|
|
33
|
+
Password: os.Getenv("DB_PASSWORD"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Validate no defaults
|
|
37
|
+
if dbConfig.Password == "admin" || dbConfig.Password == "password" || dbConfig.Password == "postgres" {
|
|
38
|
+
log.Fatal("Default credentials detected - deployment blocked")
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Blocked defaults:**
|
|
43
|
+
- `admin/admin`, `root/root`, `test/test`
|
|
44
|
+
- `postgres/postgres`, `mysql/mysql`
|
|
45
|
+
- Factory default API keys
|
|
46
|
+
|
|
47
|
+
**Tools:** Secret Scanner, GitLeaks, TruffleHog, CI/CD checks
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Output Encoding Before Interpreter Use
|
|
3
|
+
impact: HIGH
|
|
4
|
+
impactDescription: prevents XSS and injection attacks
|
|
5
|
+
tags: xss, encoding, output, html, security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Output Encoding Before Interpreter Use
|
|
9
|
+
|
|
10
|
+
XSS and injection attacks occur when unescaped user data is interpreted by browsers or other systems.
|
|
11
|
+
|
|
12
|
+
**Incorrect (no encoding):**
|
|
13
|
+
|
|
14
|
+
```go
|
|
15
|
+
// XSS vulnerability
|
|
16
|
+
http.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
|
|
17
|
+
query := r.URL.Query().Get("q")
|
|
18
|
+
fmt.Fprintf(w, "<h1>Results for: %s</h1>", query) // XSS!
|
|
19
|
+
})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Correct (context-aware encoding):**
|
|
23
|
+
|
|
24
|
+
```go
|
|
25
|
+
import "html"
|
|
26
|
+
|
|
27
|
+
// HTML context
|
|
28
|
+
http.HandleFunc("/search", func(w http.ResponseWriter, r *http.Request) {
|
|
29
|
+
query := r.URL.Query().Get("q")
|
|
30
|
+
// html.EscapeString escapes <, >, &, ', "
|
|
31
|
+
fmt.Fprintf(w, "<h1>Results for: %s</h1>", html.EscapeString(query))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
// Using html/template (auto-escapes by default)
|
|
35
|
+
tmpl := template.Must(template.New("res").Parse("<h1>Results for: {{.}}</h1>"))
|
|
36
|
+
tmpl.Execute(w, query)
|
|
37
|
+
|
|
38
|
+
// URL context
|
|
39
|
+
safeURL := url.QueryEscape(userInput)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Encoding by Context:**
|
|
43
|
+
|
|
44
|
+
| Context | Encoding |
|
|
45
|
+
|---------|----------|
|
|
46
|
+
| HTML body | `html.EscapeString()` |
|
|
47
|
+
| URL | `url.QueryEscape()` |
|
|
48
|
+
| JSON | `json.Marshal()` |
|
|
49
|
+
|
|
50
|
+
**Tools:** SonarQube, Semgrep, `html/template` (enforced escaping)
|