Skip to main content

10 práticas recomendadas de segurança em Java

Cheat sheet header java

17 de setembro de 2019

0 minutos de leitura

Nesta 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.

public void selectExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = " + parameter;
   Statement statement = connection.createStatement();
   ResultSet result = statement.executeQuery(query);

   printResult(result);
}

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. 

public void prepStatmentExample(String parameter) throws SQLException {
   Connection connection = DriverManager.getConnection(DB_URL, USER, PASS);
   String query = "SELECT * FROM USERS WHERE lastname = ?";
   PreparedStatement statement = connection.prepareStatement(query);
   statement.setString(1, parameter);
   System.out.println(statement);
   ResultSet result = statement.executeQuery();

   printResult(result);
}

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

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

application.yaml

spring:
 security:
   oauth2:
     client:
         registration:
           github:
             client-id: 796b0e5403be4729ca01
             client-secret: f379318daa27502254a05e054361074180b840a9
           okta:
             client-id: 0oa1a4wascEpYu6yk358
             client-secret: hqxj7a9lVe_TudbS2boBW7AWwxTlZiHNrJxdc_Sk
             client-name: Okta
         provider:
           okta:
             issuer-uri: https://dev-844689.okta.com/oauth2/default

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.

Lista de projetos do Snyk mostrando três repositórios com contagens de problemas de gravidade alta, média e baixa, totais de dependências, horários das atualizações e plataformas de origem.

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.

<dependency>
   <groupId>org.owasp.encoder</groupId>
   <artifactId>encoder</artifactId>
   <version>1.2.2</version>
</dependency>
String untrusted = "<script> alert(1); </script>";
System.out.println(Encode.forHtml(untrusted));

// output: <script> alert(1); </script>

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. 

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<!DOCTYPE bar [
       <!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<song>
   <artist>&xxe;</artist>
   <title>Bohemian Rhapsody</title>
   <album>A Night at the Opera</album>
</song>

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.

SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

DefaultHandler handler = new DefaultHandler() {

    public void startElement(String uri, String localName,String qName,Attributes attributes) throws SAXException {
        System.out.println(qName);
    }

    public void characters(char ch[], int start, int length) throws SAXException {
        System.out.println(new String(ch, start, length));
    }
};

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.

...
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser saxParser = factory.newSAXParser();

factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxParser.getXMLReader().setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true); 
... 

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).

Java XML Security: How to prevent External Entity (XXE) Injection attacks

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:

ObjectInputStream in = new ObjectInputStream( inputStream );
return (Data)in.readObject();

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.

private final void readObject(ObjectInputStream in) throws java.io.IOException {
   throw new java.io.IOException("Deserialized not allowed");
}

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.

FileInputStream fileInput = new FileInputStream(fileName);
ValidatingObjectInputStream in = new ValidatingObjectInputStream(fileInput);
in.accept(Foo.class);

Foo foo_ = (Foo) in.readObject();

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.

<dependency>
   <groupId>com.google.crypto.tink</groupId>
   <artifactId>tink</artifactId>
   <version>1.3.0-rc1</version>
</dependency>

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.

private void run() throws GeneralSecurityException {
   AeadConfig.register();
   KeysetHandle keysetHandle = KeysetHandle.generateNew(AeadKeyTemplates.AES256_GCM);

   String plaintext = "I want to break free!";
   String aad = "Queen";

   Aead aead = keysetHandle.getPrimitive(Aead.class);
   byte[] ciphertext = aead.encrypt(plaintext.getBytes(), aad.getBytes());
   String encr = Base64.getEncoder().encodeToString(ciphertext);
   System.out.println(encr);

   byte[] decrypted = aead.decrypt(Base64.getDecoder().decode(encr), aad.getBytes());
   String decr = new String(decrypted);
   System.out.println(decr);
}

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.

Leia mais

blog feature snyk container custom base recommendations
Blog

Conheça o Evo Agentic AppSec: segurança que acompanha o ritmo do desenvolvimento com IA

O desenvolvimento impulsionado por IA está avançando mais rápido do que os fluxos de trabalho de segurança tradicionais. O AppSec agentivo da Snyk usa agentes de segurança para encontrar, corrigir e verificar vulnerabilidades continuamente, enquanto as pessoas definem os critérios.

blog feature ai violet
Blog

VulnBump: agentes de IA conseguem corrigir dependências vulneráveis sem quebrar o build?

O VulnBump testa agentes de IA em 307 atualizações reais de dependências. Uma correção só é considerada válida se eliminar a vulnerabilidade e mantiver o build e os testes funcionando.

Blog

Modelos de ponta encontraram as vulnerabilidades. Só o atacante encontrou as cadeias.

A análise estática encontrou as falhas, mas só os testes de ataque em aplicações ativas provaram como elas poderiam ser encadeadas para causar invasões. Uma comparação entre Evo COS, Claude Security e Claude Code Security.