- Add BACKEND_SECURITY.md with security analysis and hardening guide - Add FRONTEND_TESTING_STRATEGY.md with test coverage strategy - Update API.md with new endpoints (candidates, tickets, credentials) - Update AGENTS.md documentation index
327 lines
8.6 KiB
Markdown
327 lines
8.6 KiB
Markdown
# 🔒 Backend Security Analysis - GoHorseJobs
|
||
|
||
> **Last Updated:** 2026-02-25
|
||
> **Analyzed by:** Security Review
|
||
> **Target:** Go Backend (Go 1.24, net/http, PostgreSQL)
|
||
|
||
---
|
||
|
||
## 📊 Stack Tecnológica Identificada
|
||
|
||
| Componente | Tecnologia | Status |
|
||
|------------|------------|--------|
|
||
| **Framework/Roteador** | `net/http` nativo | ✅ |
|
||
| **Banco de Dados** | PostgreSQL + `lib/pq` (SQL nativo) | ✅ |
|
||
| **Autenticação** | JWT HS256 + Cookies HttpOnly | ⚠️ Requer atenção |
|
||
| **Validação** | Manual (sem biblioteca estruturada) | ⚠️ Requer atenção |
|
||
| **Secrets** | `os.Getenv()` | ✅ |
|
||
| **Logging** | `fmt.Println` / `log.Printf` | ⚠️ Não estruturado |
|
||
| **Criptografia** | RSA-OAEP + bcrypt + AES-256-GCM | ✅ |
|
||
|
||
---
|
||
|
||
## 🚨 Vulnerabilidades Identificadas
|
||
|
||
### 1. JWT Secret Hardcoded (CRÍTICO)
|
||
|
||
**Arquivo:** `internal/utils/jwt.go:10`
|
||
|
||
```go
|
||
var jwtSecret = []byte("your-secret-key-change-this-in-production") // TODO: Move to env var
|
||
```
|
||
|
||
**Risco:** Secret hardcoded no código-fonte. Se o repositório for exposto, todos os tokens podem ser forjados.
|
||
|
||
**Status:** ❌ Não corrigido
|
||
|
||
**Correção Recomendada:**
|
||
```go
|
||
var jwtSecret = []byte(os.Getenv("JWT_SECRET"))
|
||
|
||
func init() {
|
||
if len(jwtSecret) < 32 {
|
||
panic("JWT_SECRET must be at least 32 characters")
|
||
}
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2. Logging de Dados Sensíveis (ALTO)
|
||
|
||
**Arquivo:** `internal/api/middleware/auth_middleware.go`
|
||
|
||
```go
|
||
fmt.Printf("[AUTH DEBUG] Token from Header (first 20 chars): '%s...'\n", token[:min(20, len(token))])
|
||
fmt.Printf("[AUTH DEBUG] Token VALID! Claims: sub=%v, tenant=%v, roles=%v\n", ...)
|
||
```
|
||
|
||
**Risco:** Tokens JWT e claims logados em produção. Pode vazar informações sensíveis.
|
||
|
||
**Status:** ❌ Não corrigido
|
||
|
||
**Correção Recomendada:**
|
||
```go
|
||
import "log/slog"
|
||
|
||
// Usar níveis de log
|
||
slog.Debug("Token validated", "sub", claims["sub"], "roles", claims["roles"])
|
||
|
||
// Em produção, nunca logar tokens
|
||
```
|
||
|
||
---
|
||
|
||
### 3. Falta de JTI para Revogação (MÉDIO)
|
||
|
||
O JWT não inclui `jti` (JWT ID), impossibilitando revogação individual de tokens.
|
||
|
||
**Status:** ❌ Não implementado
|
||
|
||
---
|
||
|
||
## 📋 Guia de Hardening
|
||
|
||
### 1️⃣ Prevenção de SQL Injection
|
||
|
||
**Status Atual:** ✅ Mitigado
|
||
|
||
O código usa placeholders corretamente:
|
||
```go
|
||
query := `SELECT id, full_name, email FROM users WHERE role = $1 LIMIT $2`
|
||
rows, err := s.DB.QueryContext(ctx, query, role, limit)
|
||
```
|
||
|
||
**Boas Práticas:**
|
||
- ❌ NUNCA usar `fmt.Sprintf` para construir queries
|
||
- ✅ Sempre usar `$1, $2, ...` placeholders
|
||
- ✅ Validar input antes de passar para queries
|
||
|
||
**Implementação Recomendada:**
|
||
```go
|
||
// Wrapper para validação de queries
|
||
func validateQuery(query string) error {
|
||
dangerousPatterns := []string{
|
||
`'; DROP`, `'; DELETE`, `'; UPDATE`, `'; INSERT`,
|
||
`OR 1=1`, `OR '1'='1`, `UNION SELECT`,
|
||
}
|
||
lower := strings.ToLower(query)
|
||
for _, pattern := range dangerousPatterns {
|
||
if strings.Contains(lower, strings.ToLower(pattern)) {
|
||
return fmt.Errorf("potential SQL injection detected")
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2️⃣ Segurança em Goroutines
|
||
|
||
**Código Atual:**
|
||
```go
|
||
// internal/services/application_service.go:61
|
||
go func() {
|
||
// Processamento assíncrono sem recover
|
||
}()
|
||
|
||
// internal/api/handlers/seeder_handler.go:45
|
||
go func() {
|
||
// Seeder em background sem context
|
||
}()
|
||
```
|
||
|
||
**Riscos:**
|
||
1. **Goroutine Leak**: Goroutines presas consomem memória
|
||
2. **Race Condition**: Acesso concorrente a recursos compartilhados
|
||
3. **Panic não tratado**: Crash do servidor
|
||
|
||
**Implementação Segura:**
|
||
```go
|
||
func (s *Service) ProcessAsync(ctx context.Context, data Data) error {
|
||
g, ctx := errgroup.WithContext(ctx)
|
||
|
||
g.Go(func() error {
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
slog.Error("Panic in goroutine", "error", r)
|
||
}
|
||
}()
|
||
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
default:
|
||
return s.doWork(data)
|
||
}
|
||
})
|
||
|
||
return g.Wait()
|
||
}
|
||
|
||
// Rate limiting para goroutines
|
||
type GoroutinePool struct {
|
||
sem chan struct{}
|
||
}
|
||
|
||
func NewPool(max int) *GoroutinePool {
|
||
return &GoroutinePool{sem: make(chan struct{}, max)}
|
||
}
|
||
|
||
func (p *GoroutinePool) Go(fn func()) {
|
||
p.sem <- struct{}{}
|
||
go func() {
|
||
defer func() { <-p.sem }()
|
||
fn()
|
||
}()
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 3️⃣ Validação de JWT - Hardening
|
||
|
||
**Implementação Recomendada:**
|
||
```go
|
||
package auth
|
||
|
||
import (
|
||
"crypto/subtle"
|
||
"time"
|
||
|
||
"github.com/golang-jwt/jwt/v5"
|
||
"github.com/google/uuid"
|
||
)
|
||
|
||
type SecureClaims struct {
|
||
UserID string `json:"sub"`
|
||
TenantID string `json:"tenant,omitempty"`
|
||
Roles []string `json:"roles"`
|
||
TokenID string `json:"jti"` // Para revogação
|
||
TokenType string `json:"type"` // "access" ou "refresh"
|
||
jwt.RegisteredClaims
|
||
}
|
||
|
||
func (s *JWTService) ValidateToken(tokenString string) (*SecureClaims, error) {
|
||
token, err := jwt.ParseWithClaims(tokenString, &SecureClaims{},
|
||
func(token *jwt.Token) (interface{}, error) {
|
||
// ✅ Verificar algoritmo explicitamente
|
||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||
}
|
||
return s.config.SecretKey, nil
|
||
},
|
||
jwt.WithValidMethods([]string{"HS256"}),
|
||
jwt.WithIssuer(s.config.Issuer),
|
||
jwt.WithAudience(s.config.Audience...),
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
claims, ok := token.Claims.(*SecureClaims)
|
||
if !ok || !token.Valid {
|
||
return nil, errors.New("invalid token")
|
||
}
|
||
|
||
// ✅ Verificar se foi revogado (Redis lookup)
|
||
if s.isRevoked(claims.TokenID) {
|
||
return nil, errors.New("token revoked")
|
||
}
|
||
|
||
return claims, nil
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 4️⃣ Proteção Contra Replay e CSRF
|
||
|
||
**CSRF (implementação recomendada):**
|
||
```go
|
||
func (h *Handler) WithCSRF(next http.Handler) http.Handler {
|
||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
if r.Method != "GET" && r.Method != "HEAD" && r.Method != "OPTIONS" {
|
||
csrfToken := r.Header.Get("X-CSRF-Token")
|
||
cookieToken, err := r.Cookie("csrf_token")
|
||
|
||
if err != nil || csrfToken == "" || cookieToken.Value == "" {
|
||
http.Error(w, "CSRF token missing", http.StatusForbidden)
|
||
return
|
||
}
|
||
|
||
if subtle.ConstantTimeCompare([]byte(csrfToken), []byte(cookieToken.Value)) != 1 {
|
||
http.Error(w, "CSRF token invalid", http.StatusForbidden)
|
||
return
|
||
}
|
||
}
|
||
next.ServeHTTP(w, r)
|
||
})
|
||
}
|
||
```
|
||
|
||
**Replay Attack Protection:**
|
||
```go
|
||
type TokenStore struct {
|
||
redis *redis.Client
|
||
}
|
||
|
||
func (s *TokenStore) IsReplayed(jti string, exp time.Time) bool {
|
||
key := fmt.Sprintf("used_token:%s", jti)
|
||
|
||
// Verificar se já foi usado
|
||
if s.redis.Exists(context.Background(), key).Val() > 0 {
|
||
return true // Replay detectado
|
||
}
|
||
|
||
// Marcar como usado até a expiração
|
||
ttl := time.Until(exp)
|
||
if ttl > 0 {
|
||
s.redis.Set(context.Background(), key, "1", ttl)
|
||
}
|
||
|
||
return false
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 Resumo de Riscos
|
||
|
||
| Vulnerabilidade | Severidade | Status |
|
||
|-----------------|------------|--------|
|
||
| JWT Secret Hardcoded | **CRÍTICO** | ❌ Pendente |
|
||
| Logging de Tokens | **ALTO** | ❌ Pendente |
|
||
| Falta de JTI/Revogação | **MÉDIO** | ❌ Pendente |
|
||
| Goroutine Leak | **MÉDIO** | ❌ Pendente |
|
||
| Falta de CSRF Token | **MÉDIO** | ❌ Pendente |
|
||
| SQL Injection | **BAIXO** | ✅ Mitigado |
|
||
|
||
---
|
||
|
||
## 🛠️ Plano de Ação Prioritário
|
||
|
||
| Prioridade | Ação | Esforço |
|
||
|------------|------|---------|
|
||
| **P0** | Mover `jwtSecret` hardcoded para `JWT_SECRET` env var | 1h |
|
||
| **P0** | Remover logs de tokens em produção | 30min |
|
||
| **P1** | Implementar logging estruturado (slog/zap) | 4h |
|
||
| **P1** | Adicionar `jti` aos tokens e implementar revogação | 8h |
|
||
| **P2** | Implementar CSRF tokens para operações sensíveis | 4h |
|
||
| **P2** | Adicionar middleware de rate limiting | 4h |
|
||
| **P3** | Migrar para RS256 (JWT assimétrico) para rotação de chaves | 16h |
|
||
|
||
---
|
||
|
||
## 📚 Referências
|
||
|
||
- [OWASP Go Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Go_Security_Cheat_Sheet.html)
|
||
- [JWT Best Practices](https://auth0.com/blog/jwt-authentication-best-practices/)
|
||
- [Go Concurrency Patterns](https://go.dev/blog/pipelines)
|
||
- [CWE-798: Hard-coded Credentials](https://cwe.mitre.org/data/definitions/798.html)
|
||
|
||
---
|
||
|
||
> **Nota:** Este documento deve ser revisado trimestralmente ou após mudanças significativas na arquitetura de segurança.
|