FlawOpen

flawopen.com/Type Juggling/PHP ==

O == do PHP é seguro para comparar hashes?

CWE-697: Incorrect ComparisonPágina de referência
Resposta curta

Não. Com ==, o PHP compara strings numéricas como números, então strings diferentes em notação científica podem ser iguais mesmo no PHP 8. Valide o tipo da entrada. Para valores exatos use ===; para tokens secretos ou MACs, use hash_equals() para evitar vazamento temporal dependente do conteúdo.

VULNERÁVEL
<?php
// Both strings are numeric and represent zero
$storedHash = "0e462097431906509019562988736854";
$submittedHash = "0e987654321";

// == returns true on PHP 7 and PHP 8
if ($storedHash == $submittedHash) {
    login();
}

// Request data may also have the wrong type:
$submitted = $_POST['token'] ?? null;
if ($storedHash == $submitted) {
    login();
}
// ?token[]=x supplies an array, not a string.
CORRIGIDO
<?php
$submitted = $_POST['token'] ?? null;
if (!is_string($submitted)) {
    http_response_code(400);
    exit;
}

// For secret tokens or MACs:
$tokenIsValid = hash_equals($storedToken, $submitted);
if ($tokenIsValid) {
    login();
}

// For non-secret identifiers, strict
// comparison checks both type and value:
$idsMatch = ($expectedId === $submitted);

// Passwords require password_verify(),
// not a hand-written hash comparison:
$passwordIsValid = password_verify($plain, $storedPasswordHash);

O problema dos “magic hashes”

Duas strings numéricas são comparadas numericamente pelo PHP antes e depois da versão 8. Um hash hexadecimal que começa com 0e e continua apenas com dígitos é uma string válida em notação científica cujo valor numérico é zero. Por exemplo, 0e462097431906509019562988736854 e 0e987654321 são strings diferentes, mas ambas representam zero e são iguais com ==. O PHP 8 alterou comparações entre string e número, não esse caso entre duas strings numéricas.

O PHP 8 mudou comparações entre um número e uma string não numérica: agora compara as formas em string, em vez de tratar a string como zero. Isso não removeu a comparação clássica entre duas strings numéricas 0e…. Não use == para autenticação, em nenhuma versão.

Tipos da requisição fazem parte da fronteira

Parâmetros da requisição não são necessariamente strings. Uma consulta como ?token[]=x produz um array. Passá-lo a uma API de strings pode gerar erro de tipo ou seguir um caminho inesperado, dependendo da versão do PHP e da função. Exija uma string na fronteira da requisição antes de comparar ou chamar hash_equals().

A defesa duradoura tem duas partes: valide o tipo da entrada na fronteira e depois use uma comparação adequada ao requisito. === verifica tipo e valor; hash_equals() compara strings secretas sem vazar tempo dependente do conteúdo.

Perguntas frequentes

O PHP 8 corrige o problema por completo?

Não. O PHP 8 mudou comparações string-número, mas duas strings numéricas ainda são comparadas numericamente. Strings 0e… diferentes ainda podem ser iguais. Use === para valores tipados exatos ou hash_equals() para segredos e valide o tipo da entrada em todas as versões suportadas.

Por que hash_equals() em vez de ===?

O PHP documenta hash_equals() como segura contra ataques de temporização relacionados ao conteúdo; uma comparação comum pode demorar mais ou menos conforme a posição da diferença. Passe primeiro o segredo conhecido e depois a string enviada pelo usuário. Para senhas use password_verify().

Isso acontece em outras linguagens?

Surpresas com comparação flexível também existem em JavaScript com ==; por isso === costuma ser o padrão recomendado. A forma específica de magic hash decorre da comparação de strings hexadecimais com notação científica no PHP.

Referências

Veja também O bloco unsafe do Rust ainda é seguro? strncpy() é mais segura que strcpy()? CWE vs. CVE: qual é a diferença? Mass assignment: riscos e listas seguras de campos Vazamento de estado serverless: isole dados por requisição