# Números aleatórios e tokens seguros em PHP com random_bytes

> Gerar tokens seguros em PHP com random_bytes e random_int: comprimento, codificação, armazenamento como hash, validade e funções a evitar.

Fonte: https://phpsos.de/pt/blog/sichere-tokens-php/  
Atualizado em: 25/10/2026  
Prestador: PHP SOS, Assistência de emergência PHP e segurança de servidores. Ajuda imediata por WhatsApp: https://wa.me/491736494914?text=Ol%C3%A1%2C%20PHP%20SOS%2C%20precisamos%20de%20ajuda%20com%20o%20nosso%20servidor%20PHP.%20Empresa%3A%20%E2%80%A6%20Site%3A%20%E2%80%A6

**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()`.

## Um link demasiado fácil de adivinhar

“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
<?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
<?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](https://phpsos.de/pt/blog/passwoerter-in-php-speichern/).

> **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](https://phpsos.de/pt/ratgeber/session-sicherheit-php/). Para tokens de formulário contra pedidos forjados, ver [Proteção CSRF em aplicações PHP](https://phpsos.de/pt/blog/csrf-schutz-php/). Outras ferramentas incorporadas são apresentadas em [Funções de segurança incorporadas no PHP](https://phpsos.de/pt/php/eingebaute-sicherheitsfunktionen/).

## 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](https://phpsos.de/pt/leistungen/sicherheitsaudit-penetrationstest/).


## 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

- [Manual do PHP: random_bytes](https://www.php.net/manual/de/function.random-bytes.php)
- [Manual do PHP: Random\Randomizer::getBytesFromString](https://www.php.net/manual/de/random-randomizer.getbytesfromstring.php)
- [Manual do PHP: hash_equals](https://www.php.net/manual/de/function.hash-equals.php)
- [OWASP Cheat Sheet Series](https://cheatsheetseries.owasp.org/)
