Go Security: Boas Práticas de Segurança

Introdução

Segurança em aplicações Go é fundamental para proteger dados sensíveis, prevenir ataques e garantir conformidade com regulamentações como LGPD e GDPR. Embora Go seja uma linguagem segura por design (memory safety, type safety), vulnerabilidades de segurança ainda ocorrem principalmente devido a práticas de codificação inadequadas.

Neste guia, você vai aprender as melhores práticas de segurança para desenvolver aplicações Go robustas e protegidas contra as ameaças mais comuns.

Princípios de Segurança em Go

1. Segurança por Padrão (Secure by Default)

// ❌ INSEGURO: Configurações inseguras por padrão
http.ListenAndServe(":8080", nil) // Sem timeout, sem TLS

// ✅ SEGURO: Configurações seguras por padrão
srv := &http.Server{
    Addr:         ":8443",
    Handler:      handler,
    ReadTimeout:  5 * time.Second,
    WriteTimeout: 10 * time.Second,
    IdleTimeout:  120 * time.Second,
    TLSConfig:    &tls.Config{
        MinVersion: tls.VersionTLS12,
        CurvePreferences: []tls.CurveID{
            tls.X25519,
            tls.CurveP256,
        },
        CipherSuites: []uint16{
            tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
            tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
            tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
        },
    },
}
log.Fatal(srv.ListenAndServeTLS("cert.pem", "key.pem"))

2. Princípio do Menor Privilégio

// ❌ INSEGURO: Rodando com privilégios excessivos
// Aplicação com root pode comprometer todo o sistema

// ✅ SEGURO: Executar com usuário não-privilegiado
import "os/user"

func dropPrivileges() error {
    // Criar usuário dedicado para a aplicação
    // useradd -r -s /bin/false goapp
    
    usr, err := user.Lookup("goapp")
    if err != nil {
        return err
    }
    
    uid, _ := strconv.Atoi(usr.Uid)
    gid, _ := strconv.Atoi(usr.Gid)
    
    if err := syscall.Setgid(gid); err != nil {
        return err
    }
    if err := syscall.Setuid(uid); err != nil {
        return err
    }
    
    return nil
}

Validação de Input

A validação inadequada de input é a causa #1 de vulnerabilidades. Sempre valide dados vindos de fontes externas.

Validação com go-playground/validator

package main

import (
    "fmt"
    "github.com/go-playground/validator/v10"
)

type User struct {
    Name     string `validate:"required,min=2,max=100"`
    Email    string `validate:"required,email"`
    Age      int    `validate:"gte=18,lte=120"`
    Password string `validate:"required,min=8,containsany=~!@#$%^&*"`
    Website  string `validate:"omitempty,url"`
}

func validateUser(user *User) error {
    validate := validator.New()
    
    // Validar estrutura
    if err := validate.Struct(user); err != nil {
        if validationErrors, ok := err.(validator.ValidationErrors); ok {
            for _, err := range validationErrors {
                fmt.Printf("Campo '%s' falhou na validação: %s\n", 
                    err.Field(), err.Tag())
            }
        }
        return err
    }
    
    return nil
}

func main() {
    user := &User{
        Name:     "Jo",
        Email:    "invalid-email",
        Age:      16,
        Password: "123",
    }
    
    if err := validateUser(user); err != nil {
        fmt.Println("Validação falhou:", err)
    }
}

Sanitização de Input

import (
    "html"
    "strings"
    "github.com/microcosm-cc/bluemonday"
)

// Sanitizar HTML para prevenir XSS
func sanitizeHTML(input string) string {
    p := bluemonday.UGCPolicy()
    return p.Sanitize(input)
}

// Escape para evitar XSS em templates
func escapeForHTML(input string) string {
    return html.EscapeString(input)
}

// Sanitizar strings
func sanitizeString(input string) string {
    // Remover caracteres de controle
    sanitized := strings.Map(func(r rune) rune {
        if r >= 32 && r != 127 {
            return r
        }
        return -1
    }, input)
    
    // Trim espaços
    return strings.TrimSpace(sanitized)
}

Validação de Arquivos Upload

func handleFileUpload(w http.ResponseWriter, r *http.Request) {
    // Limitar tamanho do upload (10MB)
    r.Body = http.MaxBytesReader(w, r.Body, 10<<20)
    
    file, header, err := r.FormFile("upload")
    if err != nil {
        http.Error(w, "Arquivo muito grande", http.StatusBadRequest)
        return
    }
    defer file.Close()
    
    // Validar extensão
    allowedExts := map[string]bool{
        ".jpg": true, ".jpeg": true, ".png": true, ".gif": true,
    }
    ext := strings.ToLower(filepath.Ext(header.Filename))
    if !allowedExts[ext] {
        http.Error(w, "Tipo de arquivo não permitido", http.StatusBadRequest)
        return
    }
    
    // Validar tipo MIME real
    buffer := make([]byte, 512)
    n, _ := file.Read(buffer)
    contentType := http.DetectContentType(buffer[:n])
    
    allowedTypes := map[string]bool{
        "image/jpeg": true, "image/png": true, "image/gif": true,
    }
    if !allowedTypes[contentType] {
        http.Error(w, "Tipo de conteúdo inválido", http.StatusBadRequest)
        return
    }
    
    // Gerar nome de arquivo seguro
    filename := uuid.New().String() + ext
    filepath := filepath.Join("/uploads", filename)
    
    // Salvar arquivo
    dst, err := os.Create(filepath)
    if err != nil {
        http.Error(w, "Erro ao salvar arquivo", http.StatusInternalServerError)
        return
    }
    defer dst.Close()
    
    // Copiar conteúdo
    file.Seek(0, 0)
    if _, err := io.Copy(dst, file); err != nil {
        http.Error(w, "Erro ao processar arquivo", http.StatusInternalServerError)
        return
    }
    
    fmt.Fprintf(w, "Arquivo salvo: %s", filename)
}

Autenticação Segura

Hashing de Senhas com bcrypt

import "golang.org/x/crypto/bcrypt"

// HashPassword cria um hash seguro da senha
func HashPassword(password string) (string, error) {
    // Custo 12 é um bom equilíbrio entre segurança e performance
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

// CheckPasswordHash verifica se a senha corresponde ao hash
func CheckPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

// Uso
func main() {
    password := "senha-super-secreta-123!"
    
    hash, err := HashPassword(password)
    if err != nil {
        log.Fatal(err)
    }
    
    match := CheckPasswordHash(password, hash)
    fmt.Println("Senha correta:", match) // true
}

JWT (JSON Web Tokens)

import "github.com/golang-jwt/jwt/v5"

type Claims struct {
    UserID   uint   `json:"user_id"`
    Username string `json:"username"`
    jwt.RegisteredClaims
}

func generateToken(userID uint, username string) (string, error) {
    claims := Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "minha-app",
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(os.Getenv("JWT_SECRET")))
}

func validateToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        // Verificar algoritmo
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("método de assinatura inesperado: %v", token.Header["alg"])
        }
        return []byte(os.Getenv("JWT_SECRET")), nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, errors.New("token inválido")
}

Middleware de Autenticação

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "Token não fornecido", http.StatusUnauthorized)
            return
        }
        
        // Extrair Bearer token
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "Formato de token inválido", http.StatusUnauthorized)
            return
        }
        
        claims, err := validateToken(parts[1])
        if err != nil {
            http.Error(w, "Token inválido", http.StatusUnauthorized)
            return
        }
        
        // Adicionar claims ao contexto
        ctx := context.WithValue(r.Context(), "user", claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// Uso
http.Handle("/api/protected", AuthMiddleware(http.HandlerFunc(protectedHandler)))

Autorização e Controle de Acesso

RBAC (Role-Based Access Control)

type Role string

const (
    RoleAdmin  Role = "admin"
    RoleUser   Role = "user"
    RoleGuest  Role = "guest"
)

type Permission string

const (
    PermRead   Permission = "read"
    PermWrite  Permission = "write"
    PermDelete Permission = "delete"
)

var rolePermissions = map[Role][]Permission{
    RoleAdmin:  {PermRead, PermWrite, PermDelete},
    RoleUser:   {PermRead, PermWrite},
    RoleGuest:  {PermRead},
}

func hasPermission(role Role, perm Permission) bool {
    perms, exists := rolePermissions[role]
    if !exists {
        return false
    }
    
    for _, p := range perms {
        if p == perm {
            return true
        }
    }
    return false
}

// Middleware de autorização
func RequirePermission(perm Permission) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            user := r.Context().Value("user").(*Claims)
            // Buscar role do usuário no banco...
            role := getUserRole(user.UserID)
            
            if !hasPermission(role, perm) {
                http.Error(w, "Acesso negado", http.StatusForbidden)
                return
            }
            
            next.ServeHTTP(w, r)
        })
    }
}

Proteção Contra Vulnerabilidades Comuns

SQL Injection

// ❌ INSEGURO: Concatenação de strings
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
db.Query(query) // Vulnerável a SQL Injection

// ✅ SEGURO: Prepared statements
db.Query("SELECT * FROM users WHERE email = ?", email)

// ✅ SEGURO: Com sqlx
var user User
err := db.Get(&user, "SELECT * FROM users WHERE email = $1", email)

// ✅ SEGURO: Com GORM
db.Where("email = ?", email).First(&user)

XSS (Cross-Site Scripting)

// ❌ INSEGURO: Renderização direta
template.Execute(w, map[string]string{
    "Name": userInput, // Vulnerável a XSS
})

// ✅ SEGURO: Auto-escape de templates
type PageData struct {
    Name    string
    Content template.HTML // Só use para conteúdo confiável
}

tmpl := template.Must(template.New("page").Parse(`
    <h1>{{.Name}}</h1> <!-- Auto-escapado -->
    <div>{{.Content}}</div> <!-- HTML permitido -->
`))

// ✅ SEGURO: CSP Headers
func securityHeaders(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Security-Policy", 
            "default-src 'self'; "+
            "script-src 'self' 'unsafe-inline'; "+
            "style-src 'self' 'unsafe-inline';")
        w.Header().Set("X-Content-Type-Options", "nosniff")
        w.Header().Set("X-Frame-Options", "DENY")
        w.Header().Set("X-XSS-Protection", "1; mode=block")
        w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
        next.ServeHTTP(w, r)
    })
}

CSRF (Cross-Site Request Forgery)

import "github.com/gorilla/csrf"

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", homeHandler)
    mux.HandleFunc("/transfer", transferHandler)
    
    // Proteger com CSRF
    csrfMiddleware := csrf.Protect(
        []byte(os.Getenv("CSRF_KEY")),
        csrf.Secure(true), // Apenas HTTPS em produção
    )
    
    log.Fatal(http.ListenAndServe(":8080", csrfMiddleware(mux)))
}

// No template HTML
// <input type="hidden" name="gorilla.csrf.Token" value="{{.csrfToken}}">

Insecure Deserialization

// ❌ INSEGURO: Deserialização sem validação
data, _ := ioutil.ReadFile("data.gob")
gob.NewDecoder(bytes.NewReader(data)).Decode(&obj)

// ✅ SEGURO: Validação de integridade
func safeUnmarshal(data []byte, signature []byte, key []byte, v interface{}) error {
    // Verificar HMAC
    mac := hmac.New(sha256.New, key)
    mac.Write(data)
    expectedMAC := mac.Sum(nil)
    
    if !hmac.Equal(signature, expectedMAC) {
        return errors.New("assinatura inválida")
    }
    
    return json.Unmarshal(data, v)
}

Path Traversal

// ❌ INSEGURO: Acesso direto a arquivos
http.ServeFile(w, r, "./uploads/"+filename)

// ✅ SEGURO: Validação de caminho
func safeFileHandler(w http.ResponseWriter, r *http.Request) {
    filename := r.URL.Query().Get("file")
    
    // Sanitizar e validar
    safePath := filepath.Clean(filename)
    if strings.Contains(safePath, "..") {
        http.Error(w, "Acesso negado", http.StatusForbidden)
        return
    }
    
    fullPath := filepath.Join("/app/uploads", safePath)
    
    // Verificar se está dentro do diretório permitido
    if !strings.HasPrefix(fullPath, "/app/uploads") {
        http.Error(w, "Acesso negado", http.StatusForbidden)
        return
    }
    
    http.ServeFile(w, r, fullPath)
}

Segurança de Dependências

Verificação de Vulnerabilidades

# Instalar govulncheck
go install golang.org/x/vuln/cmd/govulncheck@latest

# Verificar vulnerabilidades
govulncheck ./...

# Verificar dependências específicas
govulncheck -v ./...

Gerenciamento Seguro de Secrets

// ❌ INSEGURO: Hardcoded secrets
apiKey := "sk_live_1234567890abcdef"

// ✅ SEGURO: Environment variables
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
    log.Fatal("API_KEY não configurada")
}

// ✅ SEGURO: Secret managers
import "github.com/aws/aws-sdk-go-v2/service/secretsmanager"

func getSecret(secretName string) (string, error) {
    cfg, err := config.LoadDefaultConfig(context.Background())
    if err != nil {
        return "", err
    }
    
    client := secretsmanager.NewFromConfig(cfg)
    result, err := client.GetSecretValue(context.Background(), 
        &secretsmanager.GetSecretValueInput{
            SecretId: aws.String(secretName),
        })
    
    if err != nil {
        return "", err
    }
    
    return *result.SecretString, nil
}

Verificação de Checksums

func verifyChecksum(filepath string, expectedHash string) error {
    file, err := os.Open(filepath)
    if err != nil {
        return err
    }
    defer file.Close()
    
    hash := sha256.New()
    if _, err := io.Copy(hash, file); err != nil {
        return err
    }
    
    computedHash := hex.EncodeToString(hash.Sum(nil))
    if computedHash != expectedHash {
        return fmt.Errorf("checksum mismatch: got %s, want %s", computedHash, expectedHash)
    }
    
    return nil
}

Logging e Monitoramento de Segurança

import "github.com/rs/zerolog/log"

func securityLogger() *zerolog.Logger {
    logger := zerolog.New(os.Stdout).With().
        Timestamp().
        Str("service", "api").
        Logger()
    
    return &logger
}

// Log de eventos de segurança
func logAuthEvent(userID uint, action string, success bool) {
    logger := securityLogger()
    
    event := logger.Info()
    if !success {
        event = logger.Warn()
    }
    
    event.
        Str("event_type", "auth").
        Str("action", action).
        Uint("user_id", userID).
        Bool("success", success).
        Str("ip", getClientIP()).
        Str("user_agent", getUserAgent()).
        Msg("auth event")
}

// Middleware de audit logging
func auditMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Capturar response status
        wrapped := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
        
        next.ServeHTTP(wrapped, r)
        
        log.Info().
            Str("method", r.Method).
            Str("path", r.URL.Path).
            Str("remote_addr", r.RemoteAddr).
            Int("status", wrapped.statusCode).
            Dur("duration", time.Since(start)).
            Msg("HTTP request")
    })
}

OWASP Top 10 para Go

#VulnerabilidadePrevenção em Go
1Broken Access ControlRBAC, middleware de autorização
2Cryptographic Failuresbcrypt, TLS 1.2+, crypto/rand
3InjectionPrepared statements, ORM
4Insecure DesignPrincipio do menor privilégio
5Security MisconfigurationConfigurações seguras por padrão
6Vulnerable Componentsgovulncheck, dependabot
7Auth Failuresbcrypt, JWT seguro, MFA
8Data Integrity FailuresAssinaturas digitais, HMAC
9Logging FailuresStructured logging, SIEM
10SSRFValidação de URLs, whitelist

Próximos Passos

Agora que você domina segurança em Go, explore:

  1. Go e Kubernetes: Deploy de Containers - Segurança em orquestração
  2. Go Observability: Logs, Métricas e Traces - Monitore ameaças
  3. Go Performance: Profiling e Otimização - Segurança com performance

FAQ

Go é seguro por padrão?

Go é memory-safe e type-safe, prevenindo vulnerabilidades comuns em C/C++. No entanto, vulnerabilidades de aplicação (SQL injection, XSS) ainda ocorrem e precisam de atenção do desenvolvedor.

Como protejo contra timing attacks?

Use subtle.ConstantTimeCompare para comparações sensíveis:

if subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1 {
    // Iguais
}

Qual a melhor prática para senhas?

Use bcrypt com custo 12-14. Nunca use MD5, SHA1 ou SHA256 para senhas.

Preciso usar HTTPS em desenvolvimento?

Para features como cookies Secure e HSTS, sim. Use mkcert para certificados locais confiáveis.


Referências: