Em resumo: Em PHP, guarda as palavras-passe com password_hash() e verifica-as com password_verify(). O valor por defeito PASSWORD_DEFAULT usa atualmente o bcrypt, desde o PHP 8.4 com fator de custo 12; onde o PHP está compilado com Argon2, PASSWORD_ARGON2ID é uma boa alternativa. Os hashes MD5 ou SHA-1 antigos migram-se envolvendo-os de imediato no password_hash e substituindo-os, no início de sessão seguinte, por um hash direto.
O que uma fuga da base de dados tem a ver com os seus hashes
Depois de uma intrusão num servidor web, a base de dados é um dos primeiros alvos. Quem, como é possível com o PHP2Shell, consegue executar código no servidor lê também as credenciais da aplicação e, com elas, a tabela das contas de utilizador. Se as palavras-passe aí guardadas ficam legíveis em horas ou em anos, decide-se não no dia do ataque, mas na programação.
Surpreendentemente, muitas aplicações guardam ainda as palavras-passe com procedimentos que nunca foram concebidos para esse fim: MD5, SHA-1, um simples SHA-256 ou até cifradas de forma reversível. Estes procedimentos são rápidos, e é precisamente esse o problema. Permitem aos atacantes testar um número enorme de candidatos por segundo.
O PHP traz há muito uma solução que calcula devagar, gera automaticamente um salt para cada palavra-passe e guarda no próprio hash o procedimento e os parâmetros. Só precisa de a usar de forma consequente.
password_hash em PHP: bcrypt ou Argon2id?
O password_hash() conhece vários procedimentos. A seleção segundo o manual do PHP:
| Constante | Procedimento | Nota |
|---|---|---|
PASSWORD_DEFAULT |
atualmente bcrypt | valor por defeito, pode mudar em versões futuras |
PASSWORD_BCRYPT |
bcrypt | fator de custo por defeito 12 desde o PHP 8.4, antes 10; só contam os primeiros 72 bytes |
PASSWORD_ARGON2I |
Argon2i | desde o PHP 7.2, só se o PHP estiver compilado com suporte a Argon2 |
PASSWORD_ARGON2ID |
Argon2id | desde o PHP 7.3, mesmo requisito |
A OWASP recomenda o Argon2id como primeira escolha, com pelo menos 19 MiB de memória, duas iterações e uma thread, e o bcrypt sobretudo para sistemas existentes. Para a maioria das aplicações PHP, a diferença na prática é menor do que a diferença entre qualquer um destes procedimentos e o MD5. Mais importante é que use um deles e ajuste regularmente os parâmetros.
Um teste rápido mostra se o Argon2id está disponível:
<?php
var_dump(defined('PASSWORD_ARGON2ID'));
var_dump(password_algos());
Guardar e verificar: um modelo completo
O exemplo seguinte mostra o registo e o início de sessão com atualização automática de hashes desatualizados. A verificação do comprimento protege do limite de 72 bytes do bcrypt:
<?php
declare(strict_types=1);
const PASSWORT_ALGO = PASSWORD_DEFAULT;
function passwortSpeichern(string $passwort): string
{
if (strlen($passwort) > 72) {
throw new InvalidArgumentException('Passwort zu lang für das gewählte Verfahren.');
}
return password_hash($passwort, PASSWORT_ALGO);
}
function passwortPruefen(string $eingabe, string $gespeicherterHash, callable $hashAktualisieren): bool
{
if (!password_verify($eingabe, $gespeicherterHash)) {
return false;
}
if (password_needs_rehash($gespeicherterHash, PASSWORT_ALGO)) {
$hashAktualisieren(password_hash($eingabe, PASSWORT_ALGO));
}
return true;
}
O strlen() conta bytes, não carateres. Aqui isso é intencional, pois o limite do bcrypt refere-se a bytes. A coluna na base de dados deve, segundo o manual, comportar mais de 60 carateres; 255 é uma boa escolha. Nunca defina um salt próprio: a opção está obsoleta e é ignorada desde o PHP 8.0.
Com a mudança para o PHP 8.4, o fator de custo por defeito do bcrypt sobe de 10 para 12. Os hashes existentes são elevados automaticamente no início de sessão seguinte, graças ao password_needs_rehash(). Em sistemas muito carregados, vale a pena fazer um breve teste de carga do início de sessão antes da migração.
Meça o fator de custo no seu servidor de produção. Uma única verificação deve custar um tempo de cálculo perceptível, mas ficar claramente abaixo de um segundo, para que os inícios de sessão não se tornem um estrangulamento sob carga.
Substituir hashes MD5 e SHA-1 antigos
O verdadeiro desafio é, em geral, o parque existente. Não conhece as próprias palavras-passe, por isso não as pode simplesmente voltar a calcular. Esperar que cada utilizador volte a iniciar sessão deixa as contas inativas sem proteção. A OWASP descreve por isso um procedimento em duas fases:
- Envolver de imediato: envolver cada valor MD5 ou SHA-1 existente com
password_hash()através de um script e guardá-lo com uma marca, por exemplo numa colunahash_typ. - Verificar no início de sessão: para as contas marcadas, converter primeiro a entrada com o procedimento antigo e depois verificá-la com
password_verify()contra o valor envolvido. - Substituir diretamente: após um início de sessão bem-sucedido, gerar um hash normal a partir do texto simples e remover a marca.
- Limpar o remanescente: repor a palavra-passe nas contas que, passado um prazo definido, ainda tenham hashes envolvidos.
<?php
// Fase 1 (uma única vez, por script): $neu = password_hash($alterMd5Wert, PASSWORD_DEFAULT);
// Fases 2 e 3 no início de sessão:
if ($konto['hash_typ'] === 'md5-umhuellt') {
$ok = password_verify(md5($eingabe), $konto['passwort_hash']);
if ($ok) {
$speichere(password_hash($eingabe, PASSWORD_DEFAULT), 'direkt');
}
}
Hashes envolvidos são melhores do que valores MD5 nus, mas mais fracos do que hashes diretos. São uma solução transitória, não um objetivo. Após um incidente em que a base de dados possa ter sido afetada, esta migração não substitui o pedido aos utilizadores para alterarem as palavras-passe. Que vestígios apontam para um incidente deste tipo é descrito em Já estou comprometido? Sinais após o PHP2Shell.
Pepper, cifragem e outros caminhos errados
Algumas ideias parecem trazer mais segurança, mas trazem pouco ou prejudicam:
- Construções de hash próprias, como SHA-256 repetido com um salt gerado por si, são mais rápidas do que o bcrypt e, por isso, mais fracas.
- Palavras-passe cifradas são reversíveis. Se a chave estiver no mesmo servidor, a cifragem protege pouco após uma intrusão.
- Um pepper, ou seja, um valor secreto adicional para todas as palavras-passe, pode ser uma camada extra se for guardado separadamente da base de dados. Mas não substitui um bom procedimento de hash e dificulta mudanças posteriores.
- Pré-hashing antes do bcrypt, para contornar o limite de 72 bytes, tem, segundo a OWASP, riscos próprios. Um limite de comprimento claro ou o Argon2id é mais simples.
Também nas regras para novas palavras-passe vale a pena olhar para o estado da técnica: o comprimento protege mais do que exigências complicadas de carateres especiais, e as palavras-passe muito comuns ou já surgidas em fugas de dados devem ser rejeitadas. Mudanças forçadas a intervalos fixos, sem motivo, levam, segundo a experiência, mais a padrões previsíveis do que a palavras-passe mais fortes.
Nos sistemas de início de sessão, as palavras-passe nunca devem estar sozinhas: a autenticação de dois fatores para administradores e um travão contra tentativas em massa complementam o armazenamento. Ambos são tratados no guia Proteger logins de administração contra brute force. Para links de reposição e códigos de utilização única aplicam-se outras regras, ver Números aleatórios e tokens seguros em PHP. Uma visão geral de outras funções é dada em Funções de segurança incorporadas no PHP.
Como ajuda concretamente a PHP SOS?
- Verificamos como a sua aplicação e os seus plugins guardam as palavras-passe e encontramos procedimentos desatualizados também em funções secundárias.
- Planeamos a migração do parque existente com envolvimento, migração no início de sessão e prazo para as contas remanescentes.
- Ajustamos o fator de custo ou os parâmetros do Argon2 aos seus servidores.
- Após um incidente, apoiamos a decisão sobre reposições de palavras-passe e a sua implementação técnica.
Oferecemos a verificação de software PHP próprio como auditoria de segurança e teste de intrusão.
Perguntas frequentes
Que algoritmo usa o password_hash com PASSWORD_DEFAULT?
Atualmente, o bcrypt. O manual do PHP adverte que o valor por defeito pode mudar quando surgirem procedimentos mais fortes. Por isso, a coluna da base de dados deve comportar mais de 60 carateres; o manual indica 255 como boa escolha.
Devo cifrar as palavras-passe em vez de fazer o hash?
Não. As palavras-passe cifradas voltam a poder ser lidas com a chave, e essa chave está normalmente no mesmo servidor. Um hash de palavra-passe é uma rua de sentido único: a aplicação só pode verificar se uma entrada corresponde.
Que valor deve ter o fator de custo do bcrypt?
Desde o PHP 8.4, o valor por defeito é 12; antes era 10. A OWASP indica 10 como limite inferior e recomenda ir tão alto quanto os seus servidores suportarem. Meça no seu próprio hardware quanto tempo demora uma verificação.
O que significa o limite de 72 bytes do bcrypt?
O bcrypt só considera os primeiros 72 bytes de uma palavra-passe; tudo o que vem depois é cortado. Com carateres acentuados, um carácter conta muitas vezes dois bytes. Por isso, limite expressamente o comprimento ou use Argon2id, se o seu PHP estiver compilado com ele.
Os utilizadores têm de alterar as palavras-passe após uma intrusão?
Se não for possível excluir que a base de dados tenha sido extraída, deve promovê-lo. Bons hashes atrasam muito a quebra, mas as palavras-passe fracas continuam vulneráveis. Se existe além disso um dever de notificação, esclareça-o com o seu encarregado da proteção de dados.