Armazenando senhas com segurança
Nesta aula, você aprenderá a armazenar senhas de forma segura em PHP usando as funções password_hash e password_verify, entendendo por que hashes simples como MD5/SHA1 são inadequados, como lidar com rehash e as melhores práticas de segurança. O conteúdo inclui exemplos práticos, exercícios com respostas e referências oficiais.
Armazenar senhas de usuários de forma segura é uma das responsabilidades mais críticas no desenvolvimento web. Uma senha em texto puro ou com hash fraco é uma porta aberta para ataques de vazamento de dados. Nesta aula, você vai dominar as funções nativas do PHP password_hash() e password_verify(), entender por que algoritmos como MD5 e SHA1 são inaceitáveis, como gerenciar rehash e adotar boas práticas de segurança no armazenamento e validação de credenciais.
O PHP oferece desde a versão 5.5 um conjunto de funções específicas para senhas, que implementam algoritmos modernos e seguros, com salt automático e custo configurável. Ao final desta aula, você será capaz de implementar autenticação robusta em suas aplicações, protegendo os dados dos seus usuários contra os ataques mais comuns.
password_hash e password_verify
As funções password_hash() e password_verify() são as ferramentas nativas do PHP para criar e verificar hashes de senha. Elas utilizam algoritmos fortes como bcrypt, Argon2 ou Argon2id, que são projetados para serem computacionalmente custosos, dificultando ataques de força bruta e quebra por tabelas rainbow.
password_hash() gera um hash único para cada senha, incluindo um salt aleatório, e retorna uma string que contém todas as informações necessárias para verificação futura. Já password_verify() compara a senha fornecida com o hash armazenado, extraindo o salt e o custo do próprio hash, sem precisar de parâmetros adicionais.
Exemplo básico de criação e verificação:
O segundo parâmetro de password_hash() define o algoritmo. PASSWORD_DEFAULT usa o algoritmo mais forte disponível no PHP (atualmente bcrypt), e PASSWORD_BCRYPT é a opção clássica. Para Argon2, use PASSWORD_ARGON2I ou PASSWORD_ARGON2ID (disponível a partir do PHP 7.2 e 7.3, respectivamente). É possível passar um array de opções, como custo, para ajustar a segurança:
$hash = password_hash($senha, PASSWORD_BCRYPT, ['cost' => 12]);
// Para Argon2id:
$hash = password_hash($senha, PASSWORD_ARGON2ID, ['memory_cost' => 65536, 'time_cost' => 4, 'threads' => 1]);
?>
Por que não usar md5/sha1
MD5 e SHA1 são funções de hash criptográficas antigas, mas não são adequadas para senhas. Elas são extremamente rápidas, o que permite que um atacante tente bilhões de combinações por segundo. Além disso, não incluem salt automático, tornando os hashes vulneráveis a ataques de dicionário e tabelas rainbow (tabelas pré-computadas de hashes).
Um ataque comum é o uso de GPUs para quebrar senhas com MD5 em questão de minutos. Por isso, qualquer aplicação que ainda use md5($senha) ou sha1($senha) está em alto risco. Mesmo com salt, a velocidade desses algoritmos os torna inaceitáveis. O PHP descontinua o uso dessas funções para fins de segurança, e a documentação oficial recomenda explicitamente o uso de password_hash().
Exemplo do que NÃO fazer:
// NUNCA faça isso:
$hash = md5($senha); // inseguro
$hash = sha1($senha . 'salt'); // ainda inseguro, muito rápido
?>
Algoritmos como bcrypt, Argon2 e scrypt são deliberadamente lentos e consomem memória, tornando ataques de força bruta impraticáveis. O PHP implementa bcrypt e Argon2, e a diferença de segurança é enorme.
Rehash
Com o passar do tempo, algoritmos de hash evoluem e o custo recomendado aumenta. Por isso, é importante verificar periodicamente se o hash armazenado ainda atende aos padrões atuais. A função password_needs_rehash() permite verificar se o hash precisa ser atualizado, por exemplo, se o algoritmo mudou ou se o custo configurado aumentou.
O fluxo típico é: durante o login, após verificar a senha, verifique se o hash atual precisa de rehash. Se sim, atualize o hash no banco de dados. Isso garante que senhas antigas sejam migradas para algoritmos mais fortes de forma transparente para o usuário.
// Durante o login
if (password_verify($senha_digitada, $hash_armazenado)) {
// Verifica se o hash precisa ser atualizado
if (password_needs_rehash($hash_armazenado, PASSWORD_DEFAULT, ['cost' => 12])) {
$novo_hash = password_hash($senha_digitada, PASSWORD_DEFAULT, ['cost' => 12]);
// Atualize $novo_hash no banco de dados
}
// Login bem-sucedido
} else {
// Senha incorreta
}
?>
O rehash é uma prática recomendada para manter a segurança ao longo do tempo. Ele não requer nenhuma ação do usuário e melhora a proteção de contas existentes.
Boas práticas
Além de usar as funções corretas, é fundamental adotar um conjunto de boas práticas para garantir a segurança das senhas no contexto completo da aplicação.
- Nunca armazene senhas em texto puro ou com criptografia reversível (ex.: base64, AES). Use apenas hash com salt.
- Use
PASSWORD_DEFAULTou escolha explicitamente um algoritmo forte como Argon2id, que é resistente a ataques com GPUs. - Defina um custo adequado (ex.: cost 12 para bcrypt) que equilibre segurança e desempenho. Ajuste conforme o hardware do servidor.
- Utilize
password_needs_rehash()para migrar hashes antigos para algoritmos mais fortes. - Evite limitar o tamanho da senha (mas defina um mínimo razoável, ex.: 8 caracteres) e não aplique
trim()ou outras alterações antes de hash. - Nunca faça hash de senhas com funções como
hash()sem salt e sem usar algoritmos lentos. - Proteja o banco de dados com criptografia em repouso e acesso restrito, pois mesmo hashes fortes podem ser quebrados se o banco vazar.
- Implemente proteção contra força bruta no login (ex.: limite de tentativas, CAPTCHA) para complementar a segurança do hash.
- Use prepared statements para consultas SQL e evite injeção de SQL, que pode comprometer o banco.
Exemplo de uma classe simples de autenticação:
class Usuario {
public static function criarUsuario($email, $senha) {
$hash = password_hash($senha, PASSWORD_DEFAULT);
// Inserir no banco com prepared statement
// INSERT INTO usuarios (email, senha_hash) VALUES (?, ?)
}
public static function autenticar($email, $senha) {
// Buscar usuário pelo email
// $usuario = SELECT * FROM usuarios WHERE email = ?
if ($usuario && password_verify($senha, $usuario['senha_hash'])) {
if (password_needs_rehash($usuario['senha_hash'], PASSWORD_DEFAULT)) {
$novo_hash = password_hash($senha, PASSWORD_DEFAULT);
// Atualizar no banco
}
return true;
}
return false;
}
}
?>
Referências
- PHP: password_hash - Manual
- PHP: password_verify - Manual
- PHP: password_needs_rehash - Manual
- PHP: FAQ sobre senhas seguras
- OWASP Password Storage Cheat Sheet
- OWASP Cheat Sheet Series - Password Storage
- Bcrypt - Wikipedia
- Argon2 - Wikipedia
Exercícios
- Crie uma função em PHP que receba uma senha em texto puro e retorne o hash usando
password_hash()com custo 12. Teste com uma senha de exemplo e exiba o hash. - Escreva um script que verifique se uma senha digitada corresponde a um hash armazenado usando
password_verify(). Use um hash gerado no exercício anterior. - Explique por que usar MD5 para senhas é inseguro e cite pelo menos três ataques que são facilitados por esse uso.
- Implemente um trecho de código que, após verificar a senha com sucesso, verifique se o hash precisa de rehash e atualize-o.
- Liste e explique três boas práticas adicionais para proteger senhas de usuários em uma aplicação web.
- Usar prepared statements para evitar injeção de SQL.
- Implementar limitação de tentativas de login (ex.: 5 tentativas por minuto).
- Manter o PHP e o banco de dados atualizados com patches de segurança.
function criarHash($senha) {
return password_hash($senha, PASSWORD_BCRYPT, ['cost' => 12]);
}
$hash = criarHash('MinhaSenha@123');
echo $hash;
$hash = '$2y$12$...'; // hash do exercício 1
$senhaDigitada = 'MinhaSenha@123';
if (password_verify($senhaDigitada, $hash)) {
echo "Senha correta!";
} else {
echo "Senha incorreta!";
}
if (password_verify($senha, $hashArmazenado)) {
if (password_needs_rehash($hashArmazenado, PASSWORD_DEFAULT, ['cost' => 12])) {
$novoHash = password_hash($senha, PASSWORD_DEFAULT, ['cost' => 12]);
// Atualizar no banco de dados
}
// Login ok
}
Observações finais
Armazenar senhas com segurança é um processo contínuo. Sempre use as funções nativas do PHP e mantenha-se atualizado sobre as melhores práticas de segurança. Lembre-se de que a segurança não é um estado, mas um processo de melhoria constante.