Skip to main content

Revisão segura de código: 8 boas práticas de segurança para revisar código

20 de abril de 2020

0 minutos de leitura
Material de consulta da Snyk com oito boas práticas para revisão de segurança de código, incluindo validação de entradas, proteção de credenciais, autenticação, testes de dependências e S

Baixe o guia!

"É sempre uma boa ideia verificar se há problemas de segurança no código que você está revisando. Se você não sabe o que procurar, confira esta lista prática para orientar suas próximas revisões de código! Fazer revisões de código com qualidade é difícil, principalmente quando você não tem certeza de quais erros deve procurar. A abordagem DevSecOps antecipa os testes de segurança para que as vulnerabilidades sejam encontradas e corrigidas mais cedo, nas etapas de design, desenvolvimento ou CI/CD do fluxo de trabalho. É sempre uma boa ideia verificar se há problemas de segurança no código que você está revisando. Se você não sabe o que procurar, confira esta lista prática para orientar suas próximas revisões de código!

Ao revisar o código, lembre-se de que nem todo código é igual! Pense também no que está por trás do código que você está revisando e, portanto, nos dados e ativos que está tentando proteger. Esse conhecimento prático não é algo fácil de incluir em uma lista de verificação. No entanto, com estas dicas e o seu conhecimento do domínio, você poderá decidir onde dedicar mais tempo, onde esperar um risco maior e quais tipos de ataque considerar. Uma ótima maneira de determinar onde estão as áreas de maior risco é criar árvores de ataque, que mostram onde concentrar seus esforços primeiro e com mais intensidade.

Então, vamos começar nossa lista de revisão segura de código com 8 dicas de segurança para você verificar ao analisar futuros pull requests!

1. Higienize e valide todas as entradas

Os aplicativos web modernos precisam interagir com todo tipo de entrada de terceiros. A entrada direta de um usuário final pelo navegador, por exemplo, é um caso óbvio. Como desenvolvedores, sabemos que os usuários inserem coisas inesperadas quando podem. Validar e higienizar adequadamente as entradas diretas dos usuários é uma prática essencial para garantir que os aplicativos não estejam vulneráveis à injeção de conteúdo. Mas a entrada direta do usuário está longe de ser a única coisa que você deve verificar. Basicamente, toda entrada que vem de fora dos limites do seu sistema deve ser considerada e tratada como potencialmente prejudicial. Pense em itens como:

  • feeds de dados

  • arquivos

  • eventos — em um sistema orientado a eventos, como os que se integram a plataformas de Functions as a Service

  • respostas de dados de outros sistemas

  • cookies

Além disso, entradas que parecem estar sob seu controle à primeira vista também podem ser prejudiciais. Pense bem: se um usuário mal-intencionado conseguir se conectar diretamente ao seu banco de dados, ele terá uma porta dos fundos para inserir, por exemplo, código malicioso que será executado no seu sistema. O mesmo princípio vale para:

  • parâmetros de linha de comando

  • variáveis de ambiente

  • propriedades do sistema

  • armazenamento de dados

Todas as entradas, inclusive aquelas que parecem estar sob seu controle, devem ser validadas e higienizadas. Verifique se fazem sentido. Usar o sistema de tipos de uma linguagem com segurança de tipos pode ajudar bastante. Além disso, verifique o formato, o intervalo, o tamanho, o tipo e o nome do arquivo. Não presuma nada. As entradas dos usuários devem ser higienizadas, de preferência com uma biblioteca bem testada, antes de serem armazenadas ou usadas em qualquer lugar.

2. Nunca armazene segredos no código ou na configuração

É muito fácil armazenar credenciais, tokens ou outros segredos como variáveis ou constantes, afinal, estamos apenas testando para ver se funciona. Mas é igualmente fácil esse código acabar no repositório porque você se esqueceu de removê-lo. Recomendamos que você verifique se não há nada confidencial no código que está analisando. Se você usa um repositório de código baseado em Git, há várias ferramentas excelentes, como git-secrets, que analisam seus commits estaticamente por meio de um hook de pré-commit do Git para garantir que nenhuma senha ou informação confidencial seja enviada ao repositório. Os commits são rejeitados quando a ferramenta encontra padrões de expressão regular configurados que indicam que informações confidenciais foram armazenadas de forma inadequada. Isso pode deixar os envios um pouco mais lentos, mas vale muito a pena.

Criar regras para toda a equipe que impeçam o armazenamento de credenciais no código é uma ótima maneira de monitorar práticas inadequadas no fluxo de trabalho de desenvolvimento. Use ferramentas como Vault para ajudar a gerenciar seus segredos em produção. Por fim, considere usar uma cadeia de ferramentas de gerenciamento de identidade e usuários, como Keycloak (atualmente mantido por vários desenvolvedores da Red Hat), entre outras.

Há muitas maneiras de evitar que credenciais cheguem ao seu repositório, e o ideal é implementar o maior número possível delas. Ainda assim, existe sempre a chance de alguma informação confidencial passar despercebida. Considere também auditar seus repositórios regularmente, usando ferramentas como GitRob ou truffleHog, que analisam sua base de código em busca de informações confidenciais por meio da correspondência de padrões.

3. Teste se dependências open source de terceiros introduziram novas vulnerabilidades de segurança

O desenvolvimento de aplicativos modernos depende muito de bibliotecas de terceiros. Com gerenciadores de pacotes como npm, Maven, Gradle, PyPI ou equivalentes, temos acesso fácil a bibliotecas e frameworks disponíveis publicamente. Como desenvolvedores, queremos nos concentrar na lógica de negócios específica, não tanto em criar funcionalidades básicas. Por isso, deixar que frameworks e bibliotecas façam o trabalho pesado é uma escolha óbvia.

É bem possível que você não saiba quantas dependências diretas seu aplicativo usa. Em um projeto típico, seu código pode representar apenas 1% — o restante são bibliotecas e frameworks importados. Muito do código que vai para produção não é nosso, embora dependamos bastante dele. Também é muito provável que você não saiba quantas dependências transitivas seu aplicativo usa. Hoje, frameworks maiores dependem de outras bibliotecas, que também dependem de outras bibliotecas. Ao incluir uma única biblioteca ou framework, é provável que você esteja incorporando pelo menos uma dúzia de outras bibliotecas e/ou frameworks sem necessariamente saber. Assim, as dependências compõem a maior parte do seu aplicativo. Os atacantes têm como alvo as dependências open source cada vez mais, pois a reutilização delas permite atingir muitas vítimas com um único ataque. Por isso, é importante garantir que não haja vulnerabilidades conhecidas em toda a árvore de dependências do seu aplicativo.

Vamos usar a Snyk como exemplo. A Snyk analisa estaticamente seu projeto para encontrar dependências vulneráveis que você possa estar usando e ajuda a corrigi-las. Você pode testar seus repositórios pela interface da Snyk para encontrar problemas e também impedir que usuários adicionem novas bibliotecas vulneráveis: basta testar os pull requests e reprovar o teste se uma nova vulnerabilidade for introduzida. Também é possível automatizar a criação de pull requests com correções.

Escolha a forma de trabalhar que preferir: conecte seu repositório à interface da Snyk ou analise o projeto na sua máquina usando a CLI (consulte o guia da CLI), uma integração com seu sistema de build ou um plug-in no seu IDE. Da esquerda (a máquina local dos desenvolvedores) até a extrema direita (seu sistema em produção), incluindo todas as etapas intermediárias, analise suas dependências automaticamente para receber feedback rápido.

4. Implemente uma autenticação segura

A autenticação verifica se um usuário, serviço ou entidade (interna ou externa) é quem diz ser. Pode ser algo tão simples quanto um usuário fornecer suas credenciais ou um servidor apresentar seu certificado TLS para comprovar que é realmente o servidor que afirma ser. A autenticação não informa o que o usuário ou serviço pode fazer, mas confirma que se trata realmente daquele usuário ou serviço. Veja algumas boas práticas de autenticação que você deve considerar:

Parta do princípio de que não são quem dizem ser.

Você deve partir do princípio de que não são quem dizem ser até que apresentem as credenciais que comprovem sua identidade. Presumir que o usuário ou serviço não deve ter acesso aos seus dados é, naturalmente, a postura mais segura. Garanta que seu código reflita isso.

Exija senhas complexas

No caso dos usuários, considere ser mais flexível com os nomes de usuário, especialmente quando forem endereços de e-mail. Por exemplo, há pouco valor em distinguir Patch@snyk.io de patch@snyk.io. Já exigir senhas complexas é importante (pelo menos 1 letra maiúscula, 1 letra minúscula (a-z), 1 dígito (0-9) e 1 caractere especial) e com tamanho adequado (NIST SP800-132). Eu entendo: é difícil lembrar todas aquelas senhas longas, complicadas e aleatórias — ou não tão aleatórias. Mas vamos lá! Estamos em 2020, e os gerenciadores de senhas estão aí para ajudar!

Peça para o usuário se autenticar novamente antes de operações confidenciais

Solicitar as credenciais dos usuários antes de transferir dinheiro ou realizar ações confidenciais ajuda a mitigar ataques de falsificação de solicitação entre sites (CSRF) e sequestro de sessão. Um atacante pode realizar essas tarefas confidenciais sem nunca ter fornecido as credenciais do usuário. Embora essa medida de segurança possa ser inconveniente para os usuários, ela pode protegê-los a longo prazo.

Autenticação de cliente TLS

A autenticação de cliente TLS, também conhecida como autenticação TLS mútua, exige que tanto o navegador quanto o servidor se autentiquem, enviando seus certificados TLS durante o handshake TLS. Para isso, um usuário ou serviço obtém um certificado de cliente do servidor e o apresenta nas interações seguintes. Se estiver usando um navegador, talvez o usuário precise instalar o certificado.

5. Aplique o princípio do menor privilégio

Além da autenticação, existe a autorização. Os termos são parecidos, mas significam coisas bem diferentes. Como vimos no item 4, a autenticação comprova que um usuário ou serviço é quem diz ser. Já a autorização vai além e garante que essa pessoa ou serviço tem permissão para realizar a tarefa ou ação desejada. Sabemos que precisamos verificar isso e garantir que os usuários, serviços ou processos estejam em execução ou existam em uma função com autoridade para realizar a ação. Porém, do ponto de vista do código, é muito fácil conceder mais acesso do que o necessário.

O princípio do menor privilégio determina que cada módulo (como um processo, usuário ou programa, dependendo do contexto) só possa acessar as informações e os recursos necessários para sua finalidade legítima. Em essência, conceda às pessoas ou aos processos apenas o mínimo de privilégios e permissões de que precisam para atingir seu objetivo.

Uma ótima maneira de testar isso é criar testes unitários e de integração automatizados e específicos que avaliem não apenas o caminho feliz, mas, principalmente, os casos de segurança em que algo dá errado. Esses testes devem autenticar com sucesso, mas tentar realizar operações para as quais não têm autorização. Inclua sempre esses testes ao alterar as funções em que seu aplicativo é executado ou ao introduzir novos recursos que exigem uma função específica para serem usados.

6. Trate dados confidenciais com cuidado

Expor dados confidenciais — como informações pessoais ou números de cartão de crédito dos seus clientes — pode causar danos. Mas até um caso mais sutil pode ser igualmente prejudicial. Por exemplo, expor identificadores únicos do seu sistema é perigoso se eles puderem ser usados em outra chamada para recuperar dados adicionais.

Antes de tudo, analise cuidadosamente o design do seu aplicativo e determine se você realmente precisa desses dados. Além disso, não exponha dados confidenciais, por exemplo, em registros, no preenchimento automático ou durante a transmissão de dados.

Armazenamento de dados confidenciais

Se você precisa armazenar dados confidenciais, como informações de identificação pessoal (PII) ou dados financeiros, verifique se eles estão protegidos com criptografia adequada. Provavelmente, você quer e precisa estar em conformidade com o GDPR, mas, acima de tudo, não quer que os dados dos seus clientes sejam comprometidos. A criptografia deve usar um algoritmo robusto de criptografia de duas vias, se você precisar recuperar os dados no formato original, ou um algoritmo robusto de hash criptográfico, se precisar armazenar senhas. Não caia na armadilha de criar sua própria criptografia: descubra qual tipo de criptografia usar e conte com uma biblioteca bem testada para implementá-la. Por exemplo, use BCrypt para aplicar hash às senhas e os algoritmos de criptografia Triple DES, RSA e AES para criptografar os dados que você precisa recuperar. Mais importante ainda, revise regularmente se os algoritmos usados continuam seguros. O que é perfeitamente seguro hoje pode ser comprometido amanhã.

Lembre-se também de que dados confidenciais podem permanecer na memória. Se você alterar uma senha no seu sistema, evite armazená-la temporariamente em um tipo de dado imutável. Por exemplo, se você usar uma String em Java para armazenar a senha na memória, o valor original ficará na memória até que o coletor de lixo o remova, pois String é imutável. Nesse caso, uma matriz de bytes seria uma opção melhor.

Transmissão de dados confidenciais

Se você precisa transferir dados confidenciais, verifique se a conexão é segura. Esses dados só devem ser transferidos criptografados e por meio de TLS. Você também precisa garantir que a versão do TLS esteja atualizada. Obviamente, enviar os dados do cartão de crédito de alguém como parâmetro de consulta ou em texto simples no corpo de uma solicitação HTTP não é seguro.

Dados da sessão

Por último, mas não menos importante, considere também os dados da sessão como confidenciais. A recomendação é não armazenar dados confidenciais em cookies; em vez disso, use um identificador de sessão e armazene os dados em uma sessão gerenciada pelo servidor. Além disso, verifique se os cookies estão criptografados e têm um tamanho adequado (por exemplo, 128 bits). Confira se atributos como HttpOnly, Secure, e SameSite estão configurados corretamente no cookie e se ele expira em um prazo razoável. Se o usuário encerrar a sessão no lado do cliente, a sessão deverá ser invalidada para que não possa ser usada em outro lugar.

7. Proteja-se contra ataques conhecidos

Podemos presumir que os invasores continuarão atacando nossos aplicativos usando vetores de ataque previsíveis, conhecidos e reconhecidos. A falta geral de conhecimento sobre vulnerabilidades comuns e como elas podem ser exploradas costuma levar à repetição dos mesmos erros de segurança em códigos futuros. Consulte as vulnerabilidades do OWASP Top 10 e entenda como essas explorações comuns funcionam. Confira algumas dicas para evitar os tipos mais comuns de vulnerabilidade.

XSS

A ideia central de um ataque de Cross-site Scripting (XSS) é que um usuário insira dados maliciosos no seu aplicativo, que acabam em um contexto — como um documento HTML — no qual podem ser executados para compartilhar informações confidenciais ou realizar outras atividades maliciosas. Se você pretende permitir que os usuários enviem dados pelo seu site e que eles sejam exibidos na página, precisa aprender a prevenir ataques XSS. Há várias maneiras de reduzir as chances de ataques XSS, como codificar em HTML os caracteres perigosos. Entre eles estão &, <, >, “ e ‘. Proibir esses caracteres ou sanitizá-los impede que um invasor saia da tag HTML e execute código malicioso. Para dificultar ainda mais, podemos codificar em HTML todos os caracteres HTML recebidos, para que não sejam representados de uma forma que permita sair de uma tag HTML, como mostrado a seguir:

Entidade HTML

Codificação HTML

&

&amp;

<

&lt;

>

&gt;

“

&quot;

‘

&#x27;

Da mesma forma, podemos fazer isso com atributos de tags, manipuladores de eventos ou até propriedades de estilo. É comum usar bibliotecas de sanitização para definir uma lista de permissões com os caracteres aceitos. Usar bibliotecas de sanitização existentes também significa que não precisamos ser especialistas em segurança para considerar todos os casos possíveis. Isso é especialmente útil para desenvolvedores em início de carreira.

Injeção de SQL e NoSQL

Um dos motivos pelos quais a injeção de SQL é tão atraente para um invasor é que ela dá acesso direto aos dados que ele quer obter. Muitas vezes, um ataque é apenas uma forma de o invasor aprender sobre o sistema que está tentando invadir. Isso costuma exigir que ele trabalhe mais para descobrir onde os dados estão armazenados e como acessá-los. A injeção de SQL, por outro lado, é um mecanismo que, quando explorado com sucesso, pode dar ao invasor acesso direto a informações confidenciais armazenadas em um banco de dados. Isso não se aplica apenas a bancos de dados SQL: muitos bancos de dados NoSQL podem ser comprometidos de forma semelhante.

XSS e injeção de SQL são ataques muito comuns. Você precisa saber como funcionam e como identificá-los no seu código. A falta de sanitização de entradas em qualquer parte do sistema é um grande sinal de alerta para XSS. Para evitar injeção de SQL, verifique se a parametrização de consultas está implementada. É importante distinguir a consulta dos parâmetros. Vincular os parâmetros a um tipo específico antes de incluí-los na consulta (e sanitizá-los corretamente) evita esse tipo de ataque.

Há muitos outros tipos de vulnerabilidade aos quais seu aplicativo pode estar suscetível, alguns mais do que outros. Dedique um tempo para descobrir quais afetam mais o seu aplicativo, desde o código produzido diretamente pelas suas equipes até os tipos de bibliotecas dos quais o aplicativo depende.

8. Teste seu código-fonte estaticamente e de forma automatizada

A análise estática de código é uma ferramenta muito poderosa para desenvolvedores. O subconjunto dessas ferramentas voltado à segurança é conhecido como ferramentas de teste de segurança de aplicações estático (SAST). Ao analisar estaticamente o código escrito por você e sua equipe, uma ferramenta SAST indica se algum bug relacionado à segurança passou despercebido no código-fonte. Usar uma ferramenta SAST como o Snyk Code pode identificar problemas como injeções de SQL e vulnerabilidades no código.

Para segurança, recomendamos SAST em vez de um linter. Embora os linters possam ser muito úteis na análise estática de código (bugs, erros etc.), eles geram muitos falsos positivos. Como todos os linters se baseiam em regras e não consideram o contexto completo do código, há vários casos em que um linter aponta um bug ou problema de segurança que não existe. Ainda assim, ajustar as regras de um linter pode ajudar a evitar erros graves. O ideal é automatizar esses processos o máximo possível, por exemplo, como parte do processo de build ou quando uma nova solicitação de pull request é enviada ao repositório. O Snyk Code cuida de tudo isso para você, integrando recursos de SAST diretamente aos seus fluxos de trabalho.

Se você ainda não usa uma ferramenta SAST, pode usá-la durante uma revisão manual de código ou, melhor ainda, automatizá-la no seu processo para que as pessoas identifiquem problemas óbvios ainda mais cedo. Você também pode experimentar a ferramenta gratuita de verificação de código da Snyk para ter uma ideia rápida da segurança do seu código.

Comece a participar de desafios de Capture the Flag

Aprenda a resolver desafios de Capture the Flag assistindo sob demanda ao nosso workshop virtual introdutório.