Melhores Práticas de Segurança em Java
Falhas comuns de segurança em Java e defesas — validação de entrada, deserialização, dependências e segredos.
A maioria dos bugs de segurança em Java não é exótica. São uma verificação de entrada ausente, uma consulta SQL concatenada como string, uma senha armazenada como hash simples ou um segredo enviado ao Git. Este capítulo percorre as defesas que bloqueiam a maioria dos ataques do mundo real: valide tudo que cruza um limite de confiança, nunca construa consultas por concatenação, faça hash de senhas com uma função de derivação de chave lenta, mantenha segredos fora do código e execute com o menor privilégio que a tarefa precisar.
Valide entradas com uma lista de permissões
A primeira regra é tratar toda entrada externa como hostil até prova em contrário: parâmetros de requisição, nomes de arquivo, cabeçalhos, payloads de mensagens, qualquer coisa que cruze um limite de confiança. Prefira uma lista de permissões (aceite apenas formatos conhecidamente válidos) em vez de uma lista de bloqueio (tente bloquear os inválidos) — uma lista de bloqueio sempre deixa passar algum caso.
// Allowlist: only lowercase letters, digits and underscore, 3–16 chars.
static boolean isValidUsername(String s) {
return s != null && s.matches("[a-z0-9_]{3,16}");
}
// Constrain numbers to a sane range instead of trusting the caller.
int page = Math.clamp(requested, 1, 1000);Valide na borda do sistema e novamente em qualquer limite mais profundo que você não controla. Rejeite cedo, falhe de forma fechada e retorne um erro genérico para não revelar a regra de validação a um atacante que sonda seu endpoint. Quando você comparar com um padrão, ancore-o e mantenha-o simples — veja a introdução a expressões regulares para saber como matches verifica a string inteira, não apenas um fragmento.
Use prepared statements, nunca concatenação de strings
Injeção de SQL ainda é uma das vulnerabilidades web mais comuns e prejudiciais, e em Java é trivial de prevenir. Construa consultas com parâmetros de bind por meio de PreparedStatement; o driver envia o template da consulta e os valores separadamente, de modo que os dados do usuário nunca podem ser interpretados como SQL.
// NEVER do this — user input becomes part of the query text.
String bad = "SELECT * FROM users WHERE name = '" + name + "'";
// Do this — the value is bound, not concatenated.
String sql = "SELECT id FROM users WHERE name = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name);
try (ResultSet rs = ps.executeQuery()) {
while (rs.next()) process(rs.getLong("id"));
}
}A mesma ideia se aplica além do SQL: use APIs parametrizadas para LDAP, comandos do sistema operacional (ProcessBuilder com uma lista de argumentos, não uma string de shell) e qualquer template que mistura código com dados. Para os detalhes do JDBC, veja PreparedStatement e a introdução ao JDBC.
Faça hash de senhas com um KDF lento
As senhas nunca devem ser armazenadas em texto simples nem por trás de um hash rápido como uma única rodada de SHA-256 — GPUs modernas tentam bilhões desses por segundo. Use uma função de derivação de chave deliberadamente lenta e com sal. O JDK fornece PBKDF2; Argon2 e bcrypt são excelentes opções de terceiros.
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.SecureRandom;
byte[] salt = new byte[16];
SecureRandom.getInstanceStrong().nextBytes(salt); // unique per user
var spec = new PBEKeySpec(password, salt, 600_000, 256); // iterations, key bits
var skf = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
byte[] hash = skf.generateSecret(spec).getEncoded();
spec.clearPassword(); // wipe the secret| Abordagem | Veredicto |
|---|---|
| Texto simples / reversível | Nunca |
| MD5, SHA-1, SHA-256 simples | Rápido demais — inadequado para senhas |
| PBKDF2 / bcrypt / Argon2 com sal por usuário | Correto |
| Mesmo sal para todos os usuários | Anula o propósito do sal |
Sempre compare hashes com uma verificação em tempo constante (MessageDigest.isEqual) para que o tempo de resposta não revele o quanto um palpite estava correto.
Mantenha segredos fora do código
Chaves de API, senhas de banco de dados e chaves de assinatura não pertencem a arquivos-fonte — uma vez enviados ao Git, ficam no histórico para sempre. Leia-os do ambiente ou de um gerenciador de segredos em tempo de execução e mantenha credenciais fora de logs e mensagens de exceção.
String dbPassword = System.getenv("DB_PASSWORD");
if (dbPassword == null || dbPassword.isBlank()) {
throw new IllegalStateException("DB_PASSWORD is not configured");
}
// Hold short-lived secrets in char[]/byte[] and wipe them, not String,
// because String is immutable and lingers in the heap until GC.Use SecureRandom (não java.util.Random) para qualquer coisa sensível à segurança — tokens, sals, nonces, IDs de sessão. Random é previsível e semeável, o que torna sua saída adivinhável.
Aplique o menor privilégio e padrões seguros
Dê a cada componente apenas o acesso de que precisa e nada mais: um usuário de banco de dados somente leitura para caminhos de leitura, uma conta de serviço com escopo para um bucket, permissões de arquivo que excluem grupo e outros. Valide certificados TLS (nunca desabilite a verificação de hostname "para funcionar"), defina timeouts em cada chamada de rede e limite o tamanho de qualquer coisa que você parsear para evitar negação de serviço por entrada excessivamente grande ou deserialização.
// Never deserialize untrusted bytes with Java's native serialization.
// Prefer a data format you can validate (JSON/Protobuf) and bound its size.
HttpClient client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(5)) // fail fast, don't hang
.build();Mantenha as dependências atualizadas — a maioria das violações explora um CVE conhecido em uma biblioteca antiga, então execute um scanner (OWASP Dependency-Check, mvn versions:display-dependency-updates) no CI.
O programa abaixo reúne as ideias principais: validação por lista de permissões, alongamento de senha com sal, verificação em tempo constante e prova de que dois usuários com a mesma senha obtêm hashes diferentes.
O que observar na execução:
- A lista de permissões aceita
alice_99mas rejeita tantoRobert'); DROP TABLEquanto oabcurto demais, então entrada maliciosa ou malformada nunca chega à próxima camada. - Alongar uma senha produz um digest fixo de 32 bytes ao longo de 120.000 iterações — o custo é o que torna impraticável a força bruta sobre o hash armazenado.
verifyretornatruepara a senha correta efalsepara a errada, porque o hash candidato só coincide quando a entrada é idêntica.- Dois usuários diferentes que registram exatamente a mesma senha obtêm hashes desiguais (
same input, equal hash? false), provando que o sal aleatório por usuário cumpre sua função. MessageDigest.isEqualreportatruepara bytes idênticos efalsepara uma mudança de um caractere, fornecendo uma comparação em tempo constante que não vaza por timing.