FlawOpen

flawopen.com/Type Juggling/PHP ==

¿Es seguro PHP == para comparar hashes?

CWE-697: Incorrect ComparisonPágina de referencia
Respuesta breve

No. Con ==, PHP compara numéricamente dos cadenas numéricas; por eso cadenas distintas en notación científica pueden ser iguales incluso en PHP 8. Valida el tipo de entrada. Usa === para valores exactos y hash_equals() para tokens secretos o MAC, evitando filtraciones temporales según el contenido.

VULNERABLE
<?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.
CORREGIDO
<?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);

El problema de los “magic hashes”

PHP compara numéricamente dos cadenas numéricas tanto antes como después de PHP 8. Un hash hexadecimal que empieza por 0e y continúa solo con dígitos es una cadena válida en notación científica cuyo valor es cero. Por ejemplo, 0e462097431906509019562988736854 y 0e987654321 son cadenas distintas, pero ambas representan cero y son iguales con ==. PHP 8 cambió las comparaciones entre cadena y número, no este caso entre dos cadenas numéricas.

PHP 8 cambió las comparaciones entre un número y una cadena no numérica: ahora compara sus formas de cadena, en lugar de tratar la cadena como cero. No eliminó la comparación clásica entre dos cadenas numéricas 0e…. No uses == para autenticación, sin importar la versión.

Los tipos de la petición forman parte del límite

Los parámetros de petición no siempre son cadenas. Una consulta como ?token[]=x produce un array. Pasarlo a una API de cadenas puede causar un error de tipo o una ruta inesperada, según la versión de PHP y la función. Exige una cadena en el límite de la petición antes de comparar o llamar a hash_equals().

La defensa duradera tiene dos partes: valida el tipo en el límite y luego usa una comparación adecuada al requisito. === comprueba tipo y valor; hash_equals() compara secretos sin filtrar tiempos dependientes del contenido.

Preguntas frecuentes

¿PHP 8 lo corrige por completo?

No. PHP 8 cambió las comparaciones cadena-número, pero dos cadenas numéricas aún se comparan numéricamente. Cadenas 0e… distintas todavía pueden coincidir. Usa === para valores tipados exactos o hash_equals() para secretos y valida tipos en todas las versiones admitidas.

¿Por qué hash_equals() en vez de ===?

PHP documenta hash_equals() como segura frente a ataques de temporización sobre el contenido; una comparación normal puede variar según dónde difieran las cadenas. Pasa primero el secreto conocido y después la cadena del usuario. Para contraseñas usa password_verify().

¿También ocurre en otros lenguajes?

JavaScript también tiene sorpresas con ==; por eso === suele ser la opción predeterminada recomendada. El caso específico de magic hash surge al comparar cadenas hexadecimales con notación científica en PHP.

Referencias

Véase también ¿El bloque unsafe de Rust sigue siendo seguro? ¿strncpy() es más segura que strcpy()? CWE frente a CVE: ¿cuál es la diferencia? Mass assignment: riesgos y listas seguras de campos Filtración de estado serverless: aísla datos por solicitud