- 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
8.6 KiB
🔒 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
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:
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
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:
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:
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.Sprintfpara construir queries - ✅ Sempre usar
$1, $2, ...placeholders - ✅ Validar input antes de passar para queries
Implementação Recomendada:
// 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:
// 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:
- Goroutine Leak: Goroutines presas consomem memória
- Race Condition: Acesso concorrente a recursos compartilhados
- Panic não tratado: Crash do servidor
Implementação Segura:
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:
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):
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:
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
- JWT Best Practices
- Go Concurrency Patterns
- CWE-798: Hard-coded Credentials
Nota: Este documento deve ser revisado trimestralmente ou após mudanças significativas na arquitetura de segurança.