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
| # | Vulnerabilidade | Prevenção em Go |
|---|---|---|
| 1 | Broken Access Control | RBAC, middleware de autorização |
| 2 | Cryptographic Failures | bcrypt, TLS 1.2+, crypto/rand |
| 3 | Injection | Prepared statements, ORM |
| 4 | Insecure Design | Principio do menor privilégio |
| 5 | Security Misconfiguration | Configurações seguras por padrão |
| 6 | Vulnerable Components | govulncheck, dependabot |
| 7 | Auth Failures | bcrypt, JWT seguro, MFA |
| 8 | Data Integrity Failures | Assinaturas digitais, HMAC |
| 9 | Logging Failures | Structured logging, SIEM |
| 10 | SSRF | Validação de URLs, whitelist |
Próximos Passos
Agora que você domina segurança em Go, explore:
- Go e Kubernetes: Deploy de Containers - Segurança em orquestração
- Go Observability: Logs, Métricas e Traces - Monitore ameaças
- 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: