10 práticas recomendadas de segurança em Java
17 de setembro de 2019
0 minutos de leituraNesta edição do nosso guia rápido, vamos apresentar dez práticas recomendadas de segurança em Java para mantenedores de projetos de código aberto e desenvolvedores. Este guia é uma colaboração entre Brian Vermeer, Developer Advocate da Snyk, e Jim Manico, Java Champion e fundador da Manicode Security.
Recomendamos que você imprima o guia e também leia mais sobre cada uma das 10 dicas de segurança em Java, que explicamos em detalhes a seguir.
Vamos começar!
Problemas de segurança em Java
Embora todos queiramos escrever um código excelente, a segurança em Javanem sempre faz parte da mentalidade de quem desenvolve. No entanto, evitar problemas de segurança em Java deve ser tão importante quanto tornar sua aplicação Java eficiente, escalável e fácil de manter.
Você também deve ficar atento a vulnerabilidades recentes em Java, como a Log4Shell (CVE-2021-44228), divulgada em dezembro de 2021 e que afetou aplicações que executavam versões vulneráveis do Apache Log4j.
Neste guia, abordamos 10 problemas comuns de segurança em Java. Vamos apresentar orientações práticas e exemplos de como evitar essas vulnerabilidades comuns de segurança em Java nas aplicações que você desenvolve.
1. Use a parametrização de consultas para evitar injeções
Na versão de 2017 das 10 principais vulnerabilidades da OWASP, as injeções apareceram no topo da lista, como a vulnerabilidade número um daquele ano. Em um caso típico de injeção de SQL em Java, os parâmetros de uma consulta SQL são concatenados ingenuamente à parte estática da consulta. Veja a seguir uma execução insegura de SQL em Java, que pode ser usada por um invasor para obter mais informações do que deveria.
Se o parâmetro neste exemplo for algo como '' OR 1=1, o resultado conterá todos os itens da tabela. Isso pode ser ainda mais problemático se o banco de dados aceitar várias consultas e o parâmetro for ''; UPDATE USERS SET lastname=''.
Para evitar esse risco de segurança em Java, devemos parametrizar as consultas usando uma instrução preparada. Essa deve ser a única maneira de criar consultas ao banco de dados. Ao definir todo o código SQL e passar os parâmetros à consulta depois, o código fica mais fácil de entender. Mais importante ainda: ao separar o código SQL dos dados dos parâmetros, impedimos que entradas maliciosas manipulem a consulta.
No exemplo acima, a entrada é associada ao tipo String e, portanto, faz parte do código da consulta. Essa técnica impede que a entrada do parâmetro interfira no código SQL.
Para saber mais sobre como evitar injeções de SQL, confira este guia prático: Guia rápido sobre injeção de SQL: 8 práticas recomendadas para evitar ataques de injeção de SQL
2. Use OpenID Connect com autenticação de dois fatores
Gerenciar identidades e controlar o acesso é difícil, e falhas de autenticação costumam causar violações de dados. Na verdade, esse é o segundo item da lista das 10 principais vulnerabilidades da OWASP e, portanto, um grande risco de segurança em Java. Ao criar sua própria autenticação, você precisa considerar vários aspectos: armazenamento seguro de senhas, criptografia forte, recuperação de credenciais etc. Em muitos casos, é muito mais fácil e seguro usar soluções prontas, como o OpenID Connect. O OpenID Connect (OIDC) permite autenticar usuários em diferentes sites e aplicativos. Assim, você não precisa manter e gerenciar arquivos de senhas. O OpenID Connect é uma extensão do OAuth 2.0 que fornece informações do usuário. Além de um token de acesso, ele adiciona um token de identidade e um endpoint /userinfo que permite obter informações adicionais. Também inclui descoberta de endpoints e registro dinâmico de clientes.
Configurar o OpenID Connect com bibliotecas como o Spring Security é uma tarefa simples e comum. Garanta que sua aplicação exija autenticação de dois fatores (2FA) ou autenticação multifator (MFA) para adicionar uma camada extra de segurança ao seu sistema.
Ao adicionar as dependências oauth2-client e Spring Security à sua aplicação Spring Boot, você pode usar clientes de terceiros, como Google, GitHub e Okta, para gerenciar o OIDC. Depois de criar sua aplicação, basta conectá-la ao cliente de sua escolha, especificando-o na configuração da aplicação. Pode ser o ID e o segredo do cliente do GitHub ou do Okta, como mostrado abaixo.
pom.xml
application.yaml
3. Analise suas dependências em busca de vulnerabilidades conhecidas
É bem provável que você não saiba quantas dependências diretas sua aplicação usa. E é ainda mais provável que também não saiba quantas dependências transitivas ela tem. Isso acontece com frequência, embora as dependências componham a maior parte da aplicação. Os invasores têm como alvo as dependências de código aberto cada vez mais, pois a reutilização delas permite que um invasor mal-intencionado ataque muitas vítimas. É importante garantir que não haja vulnerabilidades conhecidas de segurança em Java em toda a árvore de dependências da sua aplicação.
O Snyk testa os artefatos de build da sua aplicação e sinaliza as dependências com vulnerabilidades conhecidas. Ele apresenta em um painel uma lista das vulnerabilidades de segurança em Java presentes nos pacotes usados pela sua aplicação.

Além disso, ele recomenda versões atualizadas ou fornece correções para resolver seus problemas de segurança, por meio de uma solicitação pull request no repositório do seu código-fonte. O Snyk também protege seu ambiente: todas as futuras solicitações pull request abertas no repositório são testadas automaticamente (por meio de webhooks) para garantir que não introduzam novas vulnerabilidades conhecidas.
O Snyk está disponível pela interface web e também por CLI. Assim, você pode integrá-lo ao seu ambiente de CI e configurá-lo para interromper o build quando houver vulnerabilidades com gravidade acima do limite definido.
Use o Snyk gratuitamente em projetos de código aberto ou em projetos privados com um número limitado de testes mensais.
4. Trate dados confidenciais com cuidado
A exposição de dados confidenciais, como dados pessoais ou números de cartão de crédito dos seus clientes, pode causar danos. Mas casos mais sutis também podem ser igualmente prejudiciais. Por exemplo, expor identificadores exclusivos no seu sistema é uma vulnerabilidade de segurança em Java se eles puderem ser usados em outra chamada para recuperar dados adicionais.
Primeiro, analise atentamente o design da sua aplicação e determine se você realmente precisa desses dados. Além disso, tome cuidado para não expor dados confidenciais, por exemplo, em registros, preenchimento automático ou transmissão de dados.
Uma maneira simples de evitar que dados confidenciais apareçam nos registros é higienizar os métodos toString() das entidades do seu domínio. Assim, você não imprime campos confidenciais por acidente. Se você usa o projeto Lombok para gerar o método toString(), experimente usar @ToString.Exclude para impedir que um campo seja incluído na saída de toString().
Tenha também muito cuidado ao expor dados externamente. Por exemplo: se um endpoint do sistema exibe todos os nomes de usuário, não é necessário mostrar o identificador interno exclusivo. Esse identificador pode ser usado para associar outras informações mais confidenciais ao usuário por meio de outros endpoints. Se você usa o Jackson para serializar e desserializar POJOs em JSON, experimente usar @JsonIgnore e @JsonIgnoreProperties para impedir que essas propriedades sejam serializadas ou desserializadas.
Se precisar enviar dados confidenciais a outros serviços, criptografe-os corretamente e garanta, por exemplo, que a conexão esteja protegida por HTTPS.
5. Higienize todas as entradas
O cross-site scripting (XSS) é um problema conhecido e costuma ser explorado em aplicações JavaScript. No entanto, Java também não está imune. XSS nada mais é do que uma injeção de código JavaScript executado remotamente. Segundo a OWASP, a regra número zero para evitar XSS é: “Nunca insira dados não confiáveis, exceto em locais permitidos”. A solução básica para esse risco de segurança em Java é evitar ao máximo dados não confiáveis e higienizar todo o restante antes de usá-lo. Um bom ponto de partida é a biblioteca de codificação Java da OWASP, que oferece vários codificadores.
Higienizar entradas de texto do usuário é óbvio. Mas e os dados que você recupera de um banco de dados, mesmo que seja seu? E se o banco de dados tiver sido comprometido e alguém tiver inserido texto malicioso em um campo ou documento?
Fique de olho também nos arquivos recebidos. A vulnerabilidade Zip-slip existe em muitas bibliotecas porque o caminho dos arquivos compactados não foi higienizado. Arquivos ZIP com caminhos como ../../../../foo.xy podem ser extraídos e, potencialmente, sobrescrever arquivos arbitrários. Embora não seja um ataque XSS, esse é um bom exemplo de por que você precisa higienizar todas as entradas. Toda entrada pode ser maliciosa e deve ser higienizada adequadamente.
6. Configure os analisadores XML para evitar XXE
Com entidades externas XML (XXE) habilitadas, é possível criar um XML malicioso, como o exemplo abaixo, e ler o conteúdo de qualquer arquivo na máquina. Não é surpresa que os ataques XXE estejam na lista das 10 principais vulnerabilidades da OWASP e sejam uma vulnerabilidade de segurança em Java que precisamos evitar. As bibliotecas XML de Java são especialmente vulneráveis à injeção XXE porque a maioria dos analisadores XML habilita entidades externas por padrão.
Uma implementação ingênua do DefaultHandler e do analisador SAX do Java, como a mostrada abaixo, analisa esse arquivo XML e revela o conteúdo do arquivo passwd. O exemplo principal aqui usa o analisador SAX do Java, mas outros analisadores, como DocumentBuilder e DOM4J, têm comportamento padrão semelhante.
Alterar as configurações padrão para não permitir entidades externas e doctypes no xerces1 ou no xerces2, respectivamente, evita esses tipos de ataque.
Para instruções práticas sobre como evitar injeções XXE maliciosas, consulte o guia rápido da OWASP sobre XXE. Ou confira meu vídeo sobre como evitar ataques de injeção de entidades externas (XXE).

7. Evite a serialização em Java
A serialização em Java permite transformar um objeto em um fluxo de bytes. Esse fluxo de bytes pode ser salvo em disco ou enviado para outro sistema. No sentido inverso, é possível desserializar um fluxo de bytes e recriar o objeto original.
O maior problema está na desserialização. Normalmente, ela é semelhante a isto:
Não há como saber o que você está desserializando antes de decodificar. Um invasor pode serializar um objeto malicioso e enviá-lo à sua aplicação. Quando você chama readObject(), os objetos maliciosos já foram instanciados. Talvez você acredite que esses ataques são impossíveis porque é preciso ter uma classe vulnerável no classpath. No entanto, considerando a quantidade de classes no classpath — incluindo seu próprio código, bibliotecas Java, bibliotecas de terceiros e frameworks —, é muito provável que haja uma classe vulnerável disponível.
A serialização em Java também é chamada de “o presente que continua sendo dado”, devido aos muitos problemas que causou ao longo dos anos. A Oracle planeja remover a serialização em Java no futuro como parte do Project Amber. No entanto, isso pode demorar, e é improvável que o problema seja corrigido nas versões anteriores. Portanto, é recomendável evitar ao máximo a serialização em Java. Se precisar implementar Serializable nas entidades do seu domínio, o ideal é implementar seu próprio método readObject(), como mostrado abaixo. Isso evita a desserialização.
Se precisar desserializar um InputStream por conta própria, use um ObjectsInputStream com restrições. Um bom exemplo é o ValidatingObjectInputStream do Apache Commons IO. Esse ObjectInputStream verifica se o objeto que está sendo desserializado é permitido.
Os problemas de desserialização de objetos não se limitam à serialização Java. A desserialização de JSON para objetos Java pode apresentar problemas semelhantes. Um exemplo desse tipo de problema de desserialização com a biblioteca Jackson está no artigo “Vulnerabilidade de desserialização do Jackson”
Para saber ainda mais, confira nosso artigo sobre serialização e desserialização em Java: explicando a vulnerabilidade de desserialização do Java.
8. Use algoritmos robustos de criptografia e hash.
Se você precisa armazenar dados confidenciais no seu sistema, é essencial garantir que a criptografia adequada esteja em vigor. Primeiro, você precisa decidir que tipo de criptografia usar — por exemplo, simétrica ou assimétrica. Também é preciso escolher o nível de segurança necessário. Uma criptografia mais robusta exige mais tempo e consome mais CPU. O mais importante é que você não precisa implementar os algoritmos de criptografia por conta própria. Criptografia é complexa, e uma biblioteca confiável resolve isso para você.
Se quisermos criptografar, por exemplo, dados de cartão de crédito, provavelmente precisaremos de um algoritmo simétrico, pois precisamos conseguir recuperar o número original. Suponha que usemos o Advanced Encryption Standard (AES), atualmente o algoritmo padrão de criptografia simétrica para órgãos federais dos EUA. Para criptografar e descriptografar, não há motivo para se aprofundar na criptografia de baixo nível do Java. Recomendamos usar uma biblioteca que cuide dessa parte para você. Por exemplo, Google Tink.
Abaixo, veja um breve exemplo de como usar a criptografia autenticada com dados associados (AEAD) com AES. Isso nos permite criptografar texto simples e fornecer dados associados que devem ser autenticados, mas não criptografados.
Para senhas, é mais seguro usar um algoritmo robusto de hash criptográfico, pois não precisamos recuperar as senhas originais, apenas comparar os hashes. De acordo com a folha de dicas de senhas da OWASP, os melhores algoritmos de hash para senhas atualmente são Argon2 e BCrypt. Em sistemas legados, o Scrypt pode ser usado até certo ponto. Os três são hashes criptográficos (funções unidirecionais) e algoritmos computacionalmente difíceis, que consomem muito tempo. É exatamente isso que você quer, pois assim os ataques de força bruta levam muito tempo.
O Spring Security oferece excelente suporte a uma ampla variedade de algoritmos. Experimente usar o Argon2PasswordEncoder e o BCryptPasswordEncoder, disponibilizados pelo Spring Security 5.0 para aplicar hash às senhas.
Um algoritmo de criptografia que hoje é robusto pode se tornar fraco daqui a um ano. Por isso, é preciso revisar a criptografia regularmente para garantir que você esteja usando o algoritmo certo para cada finalidade. Use bibliotecas de segurança confiáveis e mantenha-as atualizadas.
9. Ative o Java Security Manager
Por padrão, um processo Java não tem restrições. Ele pode acessar todo tipo de recurso, como o sistema de arquivos, a rede, processos externos e muito mais. No entanto, existe um mecanismo que controla todas essas permissões: o Java Security Manager. Por padrão, o Java Security Manager não fica ativo, e a JVM tem poderes ilimitados sobre a máquina. Embora provavelmente não queiramos que a JVM acesse certas partes do sistema, ela pode acessá-las. Mais importante ainda, o Java oferece APIs capazes de realizar ações perigosas e inesperadas.
Acho que a API mais assustadora é a Attach API. Com ela, você se conecta a outras JVMs em execução e as controla. Por exemplo, é muito fácil alterar o bytecode de uma JVM em execução se você tiver acesso à máquina. Estapublicação no blog de Nicolas Frankel mostra como isso é feito.
Ativar o Java Security Manager é fácil. Basta iniciar o Java com um parâmetro adicional para ativar o gerenciador de segurança com a política padrão: java -Djava.security.manager.
No entanto, é provável que a política padrão não atenda totalmente às necessidades do seu sistema. Talvez você precise criar uma política personalizada e fornecê-la à JVM. java -Djava.security.manager -Djava.security.policy==/foo/bar/custom.policy
Observe o sinal de igual duplicado: ele substitui a política padrão. Com apenas um sinal de igual, a política padrão é expandida com a sua política personalizada.
Para saber mais sobre permissões no JDK e como criar arquivos de política, confiraa documentação oficial do Java
Observação:Desde o lançamento do Java 17, o Security Manager está marcado como “obsoleto” devido à implementação do JEP 415. Mesmo assim, ele continua totalmente funcional no Java 17. Atualmente, a maioria dos desenvolvedores Java usa Java 8 ou Java 11 em produção. Isso significa que, na prática, embora o Security Manager vá ser removido no futuro, ele ainda é um mecanismo útil.
10. Centralize o registro de logs e o monitoramento
Segurança não se resume à prevenção. Você também precisa detectar quando algo dá errado para agir de acordo. Não importa muito qual biblioteca de logging você usa. O importante é registrar bastante informação, pois a falta de logs ainda é um grande problema, segundo o OWASP Top 10. Em geral, todo evento que possa ser auditado deve ser registrado. Exceções, logins e tentativas de login malsucedidas são exemplos óbvios, mas provavelmente você vai querer registrar cada solicitação recebida, incluindo sua origem. Assim, pelo menos você saberá o que aconteceu, quando e como, caso sofra um ataque.
É recomendável usar um mecanismo para centralizar os logs. Se você usa Log4j ou logback, é relativamente fácil conectá-los, por exemplo, a uma Elastic Stack centralizada. Com ferramentas como o Kibana, os logs de todos os servidores ou sistemas podem ficar acessíveis e pesquisáveis para investigação.
Além de registrar logs, você deve monitorar seus sistemas ativamente e armazenar essas métricas em um local centralizado e de fácil acesso. Picos de CPU ou uma carga enorme proveniente de um único endereço IP, por exemplo, podem indicar um problema ou ataque. Combine o registro centralizado de logs e o monitoramento em tempo real com alertas para receber notificações quando algo estranho acontecer.
Situações como a redefinição da senha de administrador, o acesso a um servidor interno por um IP externo ou um parâmetro de URL como ‘UNION’ são apenas alguns indicadores de que algo não está certo. Se você receber os alertas adequados sobre problemas como esses e investigar o que realmente aconteceu, terá grandes chances de evitar danos maiores e corrigir a vulnerabilidade a tempo.
Perguntas frequentes
O que é segurança em Java?
Segurança em Java são as medidas adotadas por um desenvolvedor Java para impedir que um usuário mal-intencionado comprometa uma aplicação. Ao escrever código Java robusto e seguro, o desenvolvedor protege a confidencialidade, a integridade e a disponibilidade da aplicação e dos dados.
Java representa um risco de segurança?
Java não precisa representar um risco de segurança. Se você atualizar o Java corretamente, usar apenas os módulos necessários e desenvolver suas aplicações com foco em segurança, poderá minimizar os riscos. Esta folha de dicas ajuda você a evitar vulnerabilidades de segurança em Java nas aplicações que desenvolve.
Onde fica o arquivo de segurança do Java?
O arquivo java.security faz parte do Java Runtime Environment (JRE) e contém as propriedades de segurança padrão. No Java 8 ou em versões anteriores, ele fica em $JAVA_HOME/jre/lib/security. Nas versões mais recentes do Java, o arquivo está em $JAVA_HOME/conf/security.
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.



