Segurança em Go
LidoEsta aula aborda segurança em Go, cobrindo validação de entrada, gerenciamento de segredos, análise de dependências com govulncheck e boas práticas essenciais para desenvolver aplicações seguras. O conteúdo inclui exemplos práticos, referências oficiais e exercícios para fixação.
Segurança é um aspecto fundamental no desenvolvimento de software, e em Go não é diferente. Nesta aula, vamos explorar as principais práticas e ferramentas para escrever código Go seguro, desde a validação de entradas até o gerenciamento de segredos e a análise de dependências. Você aprenderá a identificar vulnerabilidades comuns e a aplicar técnicas para mitigá-las, garantindo que suas aplicações sejam robustas contra ataques maliciosos.
Go é uma linguagem com forte tipagem e um runtime seguro, mas isso não elimina a necessidade de o desenvolvedor adotar boas práticas. Vamos mergulhar em cada subtópico, com exemplos práticos e referências a ferramentas oficiais, para que você possa aplicar esses conceitos imediatamente em seus projetos.
Validação de entrada
Validação de entrada é a primeira linha de defesa contra muitos ataques, como injeção de SQL, cross-site scripting (XSS) e command injection. Em Go, isso significa verificar se os dados fornecidos pelo usuário atendem aos critérios esperados antes de processá-los. A validação pode ser feita manualmente ou com auxílio de bibliotecas como go-playground/validator.
Além de validar o formato, é importante sanitizar a entrada para remover ou neutralizar caracteres perigosos. Por exemplo, ao exibir dados em uma página web, você deve escapar caracteres HTML para evitar XSS. Em Go, o pacote html/template faz isso automaticamente quando você usa templates.
Vejamos um exemplo de validação de entrada com a biblioteca validator:
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
type User struct {
Name string `validate:"required,min=3,max=50"`
Email string `validate:"required,email"`
Age int `validate:"gte=0,lte=130"`
}
func main() {
validate := validator.New()
user := User{
Name: "Jo",
Email: "jo@example.com",
Age: 150,
}
err := validate.Struct(user)
if err != nil {
// Exibe os erros de validação
for _, e := range err.(validator.ValidationErrors) {
fmt.Printf("Erro no campo %s: %s\n", e.Field(), e.Tag())
}
} else {
fmt.Println("Dados válidos!")
}
}
No exemplo, definimos tags de validação nas structs e usamos o validator para verificar se os dados estão corretos. Isso evita que dados malformados sejam processados.
Outra prática comum é usar expressões regulares para validar padrões específicos, como números de telefone ou CEP. O pacote regexp é poderoso, mas deve ser usado com cuidado para evitar ReDoS (ataques de negação de serviço via regex). Prefira bibliotecas consolidadas e validações simples.
Gerenciamento de segredos
Segredos são informações sensíveis como senhas, chaves de API, tokens de autenticação e credenciais de banco de dados. Armazená-los de forma segura é crucial. Em Go, você nunca deve hardcodar segredos no código-fonte, pois eles podem ser expostos em repositórios públicos ou vazados em logs.
A abordagem recomendada é usar variáveis de ambiente ou serviços de gerenciamento de segredos como AWS Secrets Manager, HashiCorp Vault ou Google Cloud Secret Manager. Em Go, você pode ler variáveis de ambiente facilmente com o pacote os.
Exemplo de leitura de segredo de ambiente:
package main
import (
"fmt"
"os"
)
func main() {
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
// Lidar com a ausência de variável
fmt.Println("API_KEY não definida")
return
}
fmt.Println("API_KEY carregada com sucesso")
}
Para ambientes de desenvolvimento, você pode usar arquivos .env com bibliotecas como godotenv, mas nunca os inclua no controle de versão. Adicione-os ao .gitignore.
Além disso, ao lidar com senhas, nunca as armazene em texto puro. Use algoritmos de hash seguros como bcrypt, scrypt ou Argon2. Em Go, a biblioteca golang.org/x/crypto/bcrypt é popular.
Exemplo de hash de senha com bcrypt:
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
password := "minhaSenhaForte"
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
panic(err)
}
fmt.Println(string(hash))
// Verificação
err = bcrypt.CompareHashAndPassword(hash, []byte(password))
if err != nil {
fmt.Println("Senha inválida")
} else {
fmt.Println("Senha correta")
}
}
Lembre-se de que o gerenciamento de segredos não se limita ao armazenamento; também inclui a rotação regular, o acesso mínimo e a auditoria de quem acessa os segredos.
Dependências (govulncheck)
Dependências de terceiros podem introduzir vulnerabilidades de segurança. O Go oferece uma ferramenta oficial chamada govulncheck que analisa seu código e suas dependências em busca de vulnerabilidades conhecidas. Ela é mantida pela equipe de segurança do Go e é recomendada para todos os projetos.
Para instalar o govulncheck, use:
go install golang.org/x/vuln/cmd/govulncheck@latest
Depois, execute-o no diretório do seu módulo:
govulncheck ./...
A ferramenta verifica as dependências do seu módulo e informa as vulnerabilidades encontradas, com links para os avisos (advisories) correspondentes. É importante executá-la regularmente, especialmente antes de lançar uma versão.
Exemplo de saída:
$ govulncheck ./...
Vulnerability #1: GO-2021-0113
A maliciously crafted input can cause a panic in golang.org/x/text/language.
More info: https://pkg.go.dev/vuln/GO-2021-0113
Found in: golang.org/x/text@v0.3.7
Fixed in: golang.org/x/text@v0.3.8
Vulnerability #2: ...
Além de usar govulncheck, mantenha suas dependências atualizadas regularmente. O comando go get -u atualiza as dependências para as versões mais recentes, mas sempre teste após atualizar.
Outra prática é usar o go mod tidy para remover dependências não utilizadas, reduzindo a superfície de ataque.
Boas práticas
Existem várias boas práticas de segurança que todo desenvolvedor Go deve seguir. Vamos listar as principais:
- Use a biblioteca padrão sempre que possível: A stdlib é bem testada e mantida pela comunidade. Evite reinventar a roda.
- Evite
os/execcom entradas não sanitizadas: Se precisar executar comandos externos, nunca concatene entradas do usuário diretamente em comandos. Use argumentos separados ou sanitize. - Proteja contra CSRF e XSS: Ao criar aplicações web, use tokens CSRF e escape saídas HTML.
- Limite o uso de
unsafe: O pacoteunsafepode quebrar garantias de tipo e memória. Use apenas quando absolutamente necessário. - Implemente timeouts: Evite operações que podem travar indefinidamente. Use contextos com timeout para chamadas HTTP, banco de dados, etc.
- Registre eventos de segurança: Mantenha logs de autenticação, autorização e operações sensíveis para auditoria.
- Use
go vete outras ferramentas estáticas: Ogo vetdetecta problemas comuns, mas para segurança, considere também ostaticcheck. - Evite informações sensíveis em logs: Nunca logue senhas, tokens ou dados pessoais.
Um exemplo de uso de context com timeout:
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println("Erro:", err)
return
}
defer resp.Body.Close()
fmt.Println("Status:", resp.Status)
}
Essas práticas ajudam a proteger sua aplicação contra ataques comuns e a garantir a integridade dos dados.
Referências
- Go Security Best Practices (Blog oficial)
- Documentação do govulncheck
- Documentação do go-playground/validator
- Documentação do bcrypt
- OWASP Top Ten
- Go Security Cheat Sheet (OWASP)
Exercícios
- Exercício 1: Crie uma função em Go que valide um endereço de e-mail usando a biblioteca
validator. A função deve retornar um erro se o e-mail for inválido. Teste com alguns exemplos. - Exercício 2: Escreva um programa que leia uma variável de ambiente chamada
DATABASE_URLe imprima uma mensagem de erro se ela não estiver definida. Use o pacoteos. - Exercício 3: Dado um hash bcrypt gerado para a senha "segredo123", escreva um programa que verifique se a senha "segredo123" é correta. Use a biblioteca
golang.org/x/crypto/bcrypt. - Exercício 4: Execute o
govulncheckem um módulo Go de exemplo (crie um módulo simples com uma dependência). Explique a saída em um comentário no código. - Exercício 5: Liste três boas práticas de segurança em Go que você aprendeu e explique como elas podem prevenir ataques comuns.
package main
import (
"fmt"
"github.com/go-playground/validator/v10"
)
func validateEmail(email string) error {
validate := validator.New()
err := validate.Var(email, "required,email")
return err
}
func main() {
emails := []string{"test@example.com", "invalid-email", ""}
for _, e := range emails {
err := validateEmail(e)
if err != nil {
fmt.Printf("%q é inválido: %v\n", e, err)
} else {
fmt.Printf("%q é válido\n", e)
}
}
}
package main
import (
"fmt"
"os"
)
func main() {
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" {
fmt.Println("Erro: DATABASE_URL não definida")
return
}
fmt.Println("DATABASE_URL carregada com sucesso")
}
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func main() {
hash := []byte("$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy") // hash gerado previamente
password := []byte("segredo123")
err := bcrypt.CompareHashAndPassword(hash, password)
if err != nil {
fmt.Println("Senha inválida")
} else {
fmt.Println("Senha correta")
}
}
# Crie um módulo: go mod init exemplo
# Adicione uma dependência: go get github.com/gin-gonic/gin
# Execute: govulncheck ./...
# A saída mostrará vulnerabilidades encontradas, com links para avisos.
# Exemplo de saída:
# Vulnerability #1: GO-2021-0113
# A maliciously crafted input can cause a panic in golang.org/x/text/language.
# More info: https://pkg.go.dev/vuln/GO-2021-0113
# Found in: golang.org/x/text@v0.3.7
# Fixed in: golang.org/x/text@v0.3.8
1. Validação de entrada: previne injeção de SQL e XSS, garantindo que dados maliciosos sejam rejeitados.
2. Uso de bcrypt para senhas: protege contra ataques de força bruta e armazenamento inseguro.
3. Uso de govulncheck: identifica vulnerabilidades em dependências, permitindo correção antes de exploração.