Situação atual: PHP2Shell. Duas vulnerabilidades críticas do PHP (CVSS 9,2) permitem executar código malicioso sem autenticação. Patches disponíveis desde 21/10/2026.O que fazer agora →

Números aleatórios e tokens seguros em PHP com random_bytes

Como os programadores geram em PHP tokens para links de palavra-passe, chaves de API e códigos de utilização única: fontes criptograficamente seguras, comprimento suficiente, armazenamento como hash e validade limitada.

Atualizado em 6 min de leituraDesenvolvimento seguro

Em resumo: Em PHP, gera tokens seguros com random_bytes(), por exemplo 32 bytes, que converte em texto com bin2hex() ou Base64-URL. Para números aleatórios num intervalo existe random_int(), e para códigos a partir de um conjunto fixo de carateres, desde o PHP 8.3, Random\Randomizer::getBytesFromString(). Os tokens guardam-se como hash, com validade limitada, usam-se uma só vez e comparam-se com hash_equals().

“Esqueceu-se da palavra-passe?” Um clique, e o sistema envia um link com um código longo. Quem conhecer o link pode definir a palavra-passe. É precisamente por isso que este código tem de ser imprevisível. Se não o for, um atacante não precisa nem da caixa de correio nem da palavra-passe antiga, apenas de tentativas suficientes.

Nas auditorias a aplicações PHP encontram-se surpreendentemente muitas vezes tokens construídos a partir da hora, do ID do utilizador ou de um gerador de números aleatórios simples. Parecem longos e aleatórios, mas não são. O mesmo vale para chaves de API, links de convite, links de partilha de documentos, códigos de confirmação e tokens CSRF.

O risco é concreto: um link de reposição previsível permite assumir o controlo de qualquer conta cujo endereço de correio eletrónico seja conhecido, sem que o titular dê por nada até já não conseguir iniciar sessão.

O PHP oferece, desde a versão 7.0, uma solução simples e segura. Só tem de ser usada de forma consequente.

random_bytes em PHP: de onde vêm os valores aleatórios seguros

Segundo o manual do PHP, o random_bytes() fornece bytes aleatórios criptograficamente seguros, adequados também para segredos duradouros como chaves. A função usa a fonte de aleatoriedade do sistema operativo, no Linux por exemplo o getrandom(), no Windows a interface CNG. Se não encontrar uma fonte adequada, lança, desde o PHP 8.2, uma Random\RandomException.

A visão geral mostra que funções servem para segredos:

Função Adequada para tokens? Nota
random_bytes() sim bytes seguros, desde o PHP 7.0
random_int() sim número seguro num intervalo, desde o PHP 7.0
Random\Randomizer com motor Secure sim o Secure é, segundo o manual, o motor por defeito
rand(), mt_rand() não previsíveis, só para fins sem relevância para a segurança
uniqid() não baseada no tempo, pensada para unicidade
md5(time()), sha1($id) não entrada adivinhável, o hash não altera isso
str_shuffle(), array_rand() não não usam um gerador seguro

Gerar tokens: comprimento e codificação

32 bytes aleatórios correspondem a 256 bits e são mais do que suficientes para quase qualquer finalidade. Para a transmissão em links ou cabeçalhos, o token tem de ser convertido em texto. São habituais duas variantes:

<?php
declare(strict_types=1);

// Hex: 64 carateres, apenas 0-9 e a-f
$tokenHex = bin2hex(random_bytes(32));

// Base64-URL: 43 carateres, seguro em URLs sem mais codificação
$tokenUrl = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');

// Número aleatório num intervalo, por exemplo para uma seleção
$index = random_int(0, 9);

// A partir do PHP 8.3: código numérico de seis dígitos, p. ex. para uma confirmação por SMS
$randomizer = new \Random\Randomizer();
$code = $randomizer->getBytesFromString('0123456789', 6);

Os códigos numéricos curtos só são aceitáveis se o número de tentativas for estritamente limitado e o código expirar ao fim de poucos minutos. Para links enviados por correio eletrónico, use sempre o token longo.

Para chaves de API que parceiros ou sistemas próprios usam de forma duradoura, duas adições provaram ser úteis. Um prefixo curto e não secreto antes da parte aleatória, por exemplo para ambiente ou finalidade, facilita o reconhecimento em registos e em achados em repositórios de código. E a chave é mostrada apenas uma vez, na criação; depois, o sistema conhece apenas o seu hash. Se uma chave se perder, é substituída, não recuperada.

Cuidado com tokens construídos por si que contêm informação, por exemplo o ID do utilizador e o tempo de expiração em Base64. Codificar não é proteger: qualquer pessoa pode ler e alterar tais tokens. Se um token tiver de transportar dados, tem de ser assinado com uma chave secreta, por exemplo com hash_hmac('sha256', ...), e a assinatura é verificada com hash_equals(). Em casos mais exigentes, são preferíveis bibliotecas verificadas a construções próprias.

Guardar, verificar e fazer expirar tokens

Um token gerado em segurança é só metade do caminho. Pelo menos igualmente importante é a forma como a aplicação lida com ele. Um procedimento provado para links de reposição e de convite:

  1. Guardar apenas o hash: na base de dados fica hash('sha256', $token); o token em si só vai por link para o utilizador.
  2. Limitar a validade: guardar o tempo de expiração e verificá-lo ao resgatar.
  3. Usar uma só vez: após a utilização bem-sucedida, apagar a entrada ou marcá-la como consumida.
  4. Invalidar em caso de alterações: após uma alteração de palavra-passe, apagar todos os tokens de reposição abertos da conta.
  5. Comparar em tempo constante: onde um token é comparado diretamente com um valor guardado, usar hash_equals(), com o valor guardado como primeiro parâmetro.
<?php
// Gerar
$token = bin2hex(random_bytes(32));
$eintrag = [
    'konto_id'   => $kontoId,
    'token_hash' => hash('sha256', $token),
    'gueltig_bis'=> (new DateTimeImmutable('+30 minutes'))->format('Y-m-d H:i:s'),
];
// enviar $token apenas no link, guardar $eintrag

// Resgatar
$gesucht = hash('sha256', (string) ($_GET['token'] ?? ''));
// procurar a entrada por token_hash com um prepared statement,
// verificar a expiração e apagar a entrada de imediato

Limite também o número de pedidos de reposição por conta e período, para que a função não possa ser abusada para o envio em massa de correio eletrónico. A resposta a um pedido de reposição não deve, além disso, revelar se o endereço indicado tem sequer uma conta.

Um hash rápido como o SHA-256 chega aqui, porque o próprio token já é longo e aleatório. Para palavras-passe escolhidas por pessoas isso não vale; aí são necessários procedimentos lentos, como explica Guardar palavras-passe em PHP com segurança.

Dica

Os tokens não pertencem aos ficheiros de registo. Verifique se o seu servidor web regista URLs completos com parâmetros de consulta e se os relatórios de erros enviam dados do pedido.

Após um incidente: renovar tokens e chaves

Quem conseguiu executar código num servidor teve, em regra, também acesso a tokens, chaves de API e dados de sessão guardados. Se após o PHP2Shell ou outro incidente existir a suspeita de uma intrusão, deve por isso renovar não só as palavras-passe, mas também tokens e chaves duradouros: acessos de API, chaves de assinatura de cookies ou links, acessos a serviços de pagamento e de expedição.

As sessões são um caso especial: o PHP gera ele próprio os IDs de sessão. Como proteger as suas definições e terminar sessões após um incidente é descrito no guia Proteger sessões em PHP. Para tokens de formulário contra pedidos forjados, ver Proteção CSRF em aplicações PHP. Outras ferramentas incorporadas são apresentadas em Funções de segurança incorporadas no PHP.

Como ajuda concretamente a PHP SOS?

  • Encontramos na sua aplicação todos os pontos em que tokens, códigos e chaves são gerados e substituímos fontes inseguras.
  • Verificamos o armazenamento, a validade e a utilização única dos links de reposição, de convite e de partilha.
  • Garantimos que os tokens não vão parar a registos, relatórios de erros ou ferramentas de análise.
  • Após um incidente, planeamos consigo a substituição ordenada de tokens e chaves.

A verificação de software PHP próprio é oferecida por nós como auditoria de segurança e teste de intrusão.

Perguntas frequentes

Porque é que o mt_rand é inadequado para tokens?

O mt_rand e o rand destinam-se a estatística e jogos, não a segredos. As suas saídas podem, em certas circunstâncias, ser previstas. Para tudo o que um atacante não deve poder adivinhar, use random_bytes ou random_int.

Que comprimento deve ter um token?

Para links de reposição de palavra-passe, convites ou chaves de API, 32 bytes aleatórios são uma escolha sólida, em texto hexadecimal 64 carateres. 16 bytes, ou seja, 128 bits, são um limite inferior comum.

O uniqid é um token seguro?

Não. O uniqid baseia-se na hora atual e destina-se a garantir unicidade, não confidencialidade. Também combinações como o md5 da hora ou do ID do utilizador são previsíveis.

Tenho de cifrar na base de dados os tokens de reposição?

Guarde, em vez do token, um hash do mesmo, por exemplo com SHA-256. Quem ler a base de dados não consegue assim construir links válidos. Como o próprio token já é longo e aleatório, não é preciso um procedimento lento como no caso das palavras-passe.

O que acontece se o sistema não encontrar uma fonte de aleatoriedade?

O random_bytes e o random_int lançam, desde o PHP 8.2, uma Random\RandomException. Trate esse caso e interrompa o processo, em vez de recorrer a uma fonte mais fraca.

Fontes

Contacto imediato

Cada hora conta. Escreva-nos agora.

Descreva brevemente o sistema, a versão do PHP e o que observou. Entramos em contacto consigo, esclarecemos a situação e trabalhamos remotamente até o incidente estar concluído e documentado.

Ajuda imediata