gohorsejobs/docs/BACKEND_SECURITY.md
Tiago Yamamoto fd9af24e22 docs: update documentation with new features and security analysis
- 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
2026-02-25 05:51:06 -06:00

8.6 KiB
Raw Blame History

🔒 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.Sprintf para 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:

  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:

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


Nota: Este documento deve ser revisado trimestralmente ou após mudanças significativas na arquitetura de segurança.