Guia de segurança em Go: 8 práticas recomendadas para desenvolvedores Go
Gerred Dillon
9 de fevereiro de 2021
0 minutos de leituraNesta edição da nossa série de guias, vamos abordar oito práticas recomendadas de segurança em Go para desenvolvedores. A linguagem Go incorpora muitos recursos integrados que incentivam práticas de desenvolvimento mais seguras — em comparação com linguagens mais antigas e de baixo nível, como C —, como coleta de lixo da memória e ponteiros fortemente tipados.
Esses recursos ajudam os desenvolvedores a evitar bugs que podem levar a explorações, eliminando a responsabilidade de gerenciar a memória por conta própria. Ainda assim, há práticas recomendadas de segurança que os programadores devem conhecer. Este guia, escrito por Eric Smalling e Gerred Dillon, com a ajuda de Dan Enman (engenheiro de software sênior da Snyk), aborda alguns dos tópicos mais comuns.
Use Go Modules
Analise dependências em busca de CVEs
Use os pacotes padrão de criptografia do Go
Use html/template para ajudar a evitar ataques XSS
Uso de subshell
Evite unsafe e cgo
Use reflexão com moderação
Reduza a superfície de ataque do contêiner
1. Use Go Modules
O sistema Go Modules é o sistema oficial de gerenciamento de dependências desde a versão 1.11; os sistemas mais antigos Vendor e Dep foram descontinuados. O Go Modules permite fixar versões de dependências, inclusive de módulos transitivos, e também protege contra alterações inesperadas nos módulos por meio do banco de dados de checksums go.sum.
Primeiro, inicialize o projeto executando go mod init [namespace/project-name] no diretório de nível mais alto.
Isso criará no diretório atual um arquivo chamado go.mod, que contém o nome do projeto e a versão do Go que você está usando. Se o código-fonte tiver importações de pacotes, basta executar go build(ou test, install etc.) para atualizar o arquivo go.mod com os módulos usados, incluindo suas versões. Você também pode usar go get para atualizar suas dependências para versões específicas; isso também atualizará o arquivo go.mod.
Exemplo de arquivo go.mod:
Observe que também foi criado um arquivo chamado go.sum. Esse arquivo contém uma lista de hashes de cada módulo usado, que o Go utiliza para validar se os mesmos binários são usados em todas as compilações. Os arquivos go.mod e go.sum devem ser incluídos no controle de versão junto com o código da aplicação.
O tutorial Using Go Modules, publicado no blog oficial do Go, é um excelente recurso para aprender mais sobre o Go Modules, inclusive sobre como fixar versões de dependências transitivas, remover dependências não utilizadas e muito mais.
2. Analise dependências em busca de CVEs
Como acontece na maioria dos projetos, o volume de código nos módulos dos quais sua aplicação depende costuma ser maior que o da própria aplicação. Essas dependências externas são um vetor comum para a introdução de vulnerabilidades de segurança. Ferramentas como a Snyk — com o apoio do nosso amplo Banco de dados de vulnerabilidades — podem testar esses grafos de dependências em busca de vulnerabilidades conhecidas, sugerir atualizações para corrigir os problemas encontrados e até monitorar continuamente seus projetos para detectar novas vulnerabilidades.
Por exemplo, basta executar snyk test em uma aplicação Go para analisar os módulos e informar as CVEs conhecidas, além de indicar versões corrigidas para as quais você pode atualizar. Além disso, as ferramentas web da Snyk podem monitorar seus repositórios do GitHub diretamente e de forma contínua, alertando você sobre vulnerabilidades descobertas no futuro, mesmo que você não tenha alterado o código nem executado uma compilação de CI.


3. Prefira os pacotes padrão de criptografia do Go aos de terceiros
Os pacotes de criptografia da biblioteca padrão do Go são amplamente auditados por pesquisadores de segurança. Como eles não cobrem todos os casos, porém, você pode se sentir tentado a usar pacotes de terceiros.
Assim como não é recomendável criar seus próprios algoritmos criptográficos, tenha muito cuidado com bibliotecas criptográficas de terceiros: talvez elas não tenham sido auditadas com o mesmo rigor. Saiba de onde vem o código.
4. Use html/template para ajudar a evitar ataques XSS
Strings sem filtragem retornadas a um cliente web usando io.WriteString() ou o pacote text/template podem expor seus usuários a ataques de cross-site scripting (XSS). Isso acontece porque todas as tags HTML nas strings retornadas são renderizadas no fluxo de saída sem codificação. Além disso, se não for definido explicitamente, o cabeçalho de resposta Content-Type: plain/text pode estar incorreto.
Usar o pacote html/template é uma maneira simples de codificar automaticamente para a web o conteúdo retornado, em vez de tentar garantir que você fez isso manualmente na lógica da aplicação. A documentação do OWASP/GO-SCP tem um capítulo excelente sobre o assunto, com exemplos.
5. Uso de subshell
Em Go, um subshell basicamente oferece acesso direto ao shell do sistema e seu uso costuma ficar restrito a aplicações de linha de comando. Sempre que possível, prefira soluções implementadas nativamente em código Go, usando os módulos adequados.
Se você precisar usar um subshell, tome cuidado para sanitizar todos os dados externos que possam ser enviados a ele, assim como os dados retornados, para evitar que sua aplicação exponha detalhes desnecessários do sistema subjacente. Essa precaução é semelhante à que você tomaria para se proteger de ataques a templates renderizados (consulte a seção 4 acima) ou de injeção de comandos SQL. Considere também que executar um processo externo como parte de uma thread de requisição da aplicação pode ter outros efeitos colaterais que você não consegue controlar pelo código Go, como alterações no sistema de arquivos, chamadas a dependências externas ou mudanças no cenário de segurança que podem bloquear essas chamadas — por exemplo, limites impostos pela execução em um contêiner ou por ferramentas como AppArmor e SELinux.
6. Tenha cuidado com unsafe e cgo
Assim como a linguagem C, Go permite o uso de variáveis do tipo ponteiro — mas com segurança estrita de tipos para proteger os desenvolvedores contra efeitos colaterais acidentais ou até maliciosos. Em C, você pode sempre definir o ponteiro void*, que não tem tipo atribuído. Para fazer algo semelhante em Go, use o pacote padrão apropriadamente chamado unsafe, que permite contornar as restrições de segurança de tipos. A documentação do Go geralmente desaconselha o uso de unsafe, pois ele permite acesso direto à memória. Combinado com dados de usuários, isso pode permitir que invasores violem a segurança de memória do Go.
O uso de cgo também exige atenção. Esse comando poderoso permite integrar bibliotecas C arbitrárias à sua aplicação Go. Como qualquer ferramenta poderosa, o cgo deve ser usado com extrema cautela: você está confiando que uma dependência totalmente externa, escrita em uma linguagem insegura, foi implementada corretamente. Se houver bugs ou rotinas maliciosas nesse código externo, a proteção de memória do Go não poderá ajudar. É possível desativar o cgo definindo CGO_ENABLED=0 na compilação. Essa costuma ser uma opção segura se você não precisar dele explicitamente, já que a maioria das bibliotecas modernas do Go é escrita em Go puro.
7. Reflexão
Go é uma linguagem fortemente tipada, o que significa que os tipos das variáveis são importantes. Às vezes, você precisa obter informações sobre o tipo ou o valor de uma variável durante a execução do código. O Go oferece o pacote `reflect`, que permite consultar e manipular o tipo e o valor de uma variável de qualquer tipo. Por exemplo, você pode verificar se uma variável é de determinado tipo ou contém certas propriedades ou funções.
Embora a reflexão possa ser útil, ela também aumenta o risco de erros de tipo em tempo de execução no código Go. Se você tentar modificar uma variável refletida de uma forma não permitida (por exemplo, definir um valor que não pode ser definido em uma struct), seu código entrará em panic. Também pode ser difícil entender bem o fluxo do código e os vários tipos de tipos e valores que estão sendo refletidos. Por fim, ao trabalhar com tipos ou valores refletidos, talvez seja necessário fazer verificações de tipo, o que pode deixar o código confuso e levar a erros em tempo de execução.
A reflexão pode ser uma ferramenta poderosa, mas, com o sistema de tipos e interfaces do Go, deve ser usada raramente, pois pode causar problemas inesperados com facilidade.
8. Reduza a superfície de ataque do contêiner
Muitas aplicações Go não têm dependências externas e foram projetadas para rodar em contêineres. Por isso, devemos reduzir o sistema de arquivos disponível para elas usando algumas técnicas de criação de imagens. Uma das maneiras mais fáceis de fazer isso é usar um Dockerfile multistage: compilamos a aplicação em uma etapa de build e depois usamos uma imagem base scratch para a imagem final de implantação.
Veja o exemplo de Dockerfile a seguir:
Se você ainda não conhece os Dockerfiles, eles são instruções passo a passo que praticamente qualquer compilação de imagem OCI pode usar para criar imagens. A documentação está disponível aqui. Este é um Dockerfile multistage, com duas etapas distintas: a etapa de build e a etapa da imagem final, em tempo de execução.
Etapa 1, linhas 1–12: etapa de build
Partindo da imagem base oficial golang:1.15, nesta etapa definimos algumas variáveis de ambiente e compilamos nossa aplicação Go. Ao final dessa etapa, uma imagem temporária será armazenada em cache com o rótulo build, que poderemos usar mais adiante.
Talvez você esteja se perguntando por que estamos passando todas essas variáveis de ambiente e argumentos para a compilação:
GOPATH=””: Limpando esta variável (que foi definida na imagem base
golang:1.15), já que ela não é necessária ao usar Go Modules.CGO_ENABLED=0: desativa ocgo(consulte a seção 6 acima).GOOS=linux: informa explicitamente ao Go que a compilação deve ser feita para o sistema operacional Linux.GOARCH=amd66: informa explicitamente ao Go que a compilação deve ser feita para a arquiteturaamd64(Intel).-trimpath: remove as informações de caminho do sistema de arquivos do binário.ldflag -s: omite a tabela de símbolos e as informações de depuração.ldflag -w: omite a tabela de símbolos DWARF.
Esse conjunto de configurações gera, basicamente, o arquivo binário mais enxuto possível. Algumas delas, porém, podem não ser adequadas à sua aplicação; escolha conforme necessário.
Etapa 2, linhas 13–10: etapa da imagem em tempo de execução
Nesta etapa, copiamos apenas o binário estático e os arquivos /etc/passwd da etapa build para um sistema de arquivos scratch vazio, definimos a propriedade adequada e especificamos o comando que será executado quando o contêiner iniciar.
Para criar essa imagem, basta executar o comando a seguir no mesmo diretório do Dockerfile:
Observação: o . no fim da linha de compilação é importante: ele informa ao sistema de build onde encontrar o Dockerfile e quaisquer outros arquivos referenciados por ele.
A imagem resultante terá um sistema de arquivos com exatamente dois arquivos: nossa aplicação e o arquivo passwd, que contém o usuário moby (o nome do usuário não importa; o importante é não executar nada como root no contêiner). Não haverá sh, ps nem outros arquivos que um invasor possa explorar. É claro que, se você precisar de outros arquivos para executar sua aplicação, deverá incluí-los ou montá-los durante a execução.
Baixe o guia de segurança em Go aqui!
Comece a jogar Capture the Flag
Aprenda a resolver desafios de Capture the Flag assistindo à gravação sob demanda do nosso workshop virtual introdutório.
