En resumen: el type juggling de PHP es la conversión automática de tipos que, en la comparación laxa con ==, decide el resultado. Desde PHP 8.0 las reglas son más estrictas, pero las cadenas numéricas se siguen comparando como números, y true, false y null siguen reglas propias. Para las comprobaciones relevantes para la seguridad rige, por tanto: ===, in_array(..., true), match en lugar de switch, hash_equals() para tokens y password_verify() para contraseñas.
Una comparación que es verdadera aunque no debería serlo
Muchas vulnerabilidades de las aplicaciones PHP no están en funciones complicadas, sino en un solo carácter: un == donde debía haber un ===. El código parece correcto, las pruebas con entradas normales pasan y, aun así, en determinadas condiciones la comprobación deja pasar algo que debería detener.
La causa es una propiedad de PHP que en el día a día resulta práctica: los valores se convierten a otro tipo cuando hace falta. Así, una cadena de un formulario puede compararse sin rodeos con un número de la base de datos. En inicios de sesión, tokens, roles o campos de estado, justo esa generosidad puede resultar peligrosa.
Cómo compara PHP con ==
El manual de PHP describe con detalle las reglas de las comparaciones laxas. Para la seguridad, son importantes sobre todo estos casos:
| Comparación | PHP 7 | PHP 8 | Motivo |
|---|---|---|---|
0 == "a" |
verdadero | falso | desde PHP 8.0 se comparan como cadenas |
"1" == "01" |
verdadero | verdadero | ambas numéricas, comparación como número |
"10" == "1e1" |
verdadero | verdadero | la notación exponencial es numérica |
100 == "1e2" |
verdadero | verdadero | número frente a cadena numérica |
null == false |
verdadero | verdadero | comparación como valor booleano |
"abc" == true |
verdadero | verdadero | una cadena no vacía cuenta como verdadera |
El cambio de PHP 8.0 desactivó toda una clase de errores: antes, una cadena no numérica se convertía en 0 al compararla con un número. Eso afectaba también a las sentencias switch. Pero las demás filas de la tabla muestran que las comparaciones laxas siguen guardando sorpresas.
También los operadores <, > y el operador de comparación <=> siguen estas reglas. En las ordenaciones suele ser inofensivo, pero en comprobaciones como «importe menor que el límite» con valores de la petición, el tipo debería estar fijado de antemano.
Especialmente traicionera es la fila de la notación exponencial. Los valores hash y los tokens suelen estar formados por cifras y letras. Si un valor así parece por casualidad un número en notación exponencial, == puede comparar como iguales dos hashes distintos. Ese es el trasfondo de muchos informes de elusión de inicios de sesión en aplicaciones PHP antiguas.
Type juggling de PHP en comprobaciones de seguridad típicas
Las comparaciones laxas se esconden en más lugares que solo el operador ==:
in_array()yarray_search()comparan de forma laxa si no se indica el tercer parámetro. Una lista de permitidos con roles o ID resulta así más blanda de lo que se piensa.switchcompara de forma laxa. La expresiónmatch, disponible desde PHP 8.0, compara en cambio, según el manual, con seguridad de tipos mediante===y lanza un error si ninguna rama coincide.- Las entradas JSON entregan tipos reales. Un campo que usted espera como cadena puede llegar como
trueo como número y coincidir sorprendentemente con==. strcmp()para secretos no es adecuada, porque el tiempo de ejecución depende del contenido y los casos de error se evalúan fácilmente mal.- Comprobación de valores de retorno: funciones como
strpos()devuelven0para una coincidencia al principio yfalsepara ninguna coincidencia.if (!strpos(...))confunde ambas cosas.
Comparaciones seguras: la confrontación
Los ejemplos siguientes muestran en cada caso la variante insegura y la segura:
<?php
declare(strict_types=1);
// Comprobar el rol
$erlaubteRollen = ['admin', 'redaktion'];
in_array($rolle, $erlaubteRollen); // inseguro: laxo
in_array($rolle, $erlaubteRollen, true); // seguro: estricto
// Evaluar el estado
$aktion = match ($status) { // estricto, error ante valor desconocido
'offen' => 'bearbeiten',
'erledigt' => 'archivieren',
};
// Comprobar el token de un enlace
if ($tokenAusLink == $gespeichertesToken) { /* inseguro */ }
if (is_string($tokenAusLink) && hash_equals($gespeichertesToken, $tokenAusLink)) { /* seguro */ }
// Comprobar la contraseña
if (md5($eingabe) == $hash) { /* inseguro: algoritmo débil y comparación laxa */ }
if (password_verify($eingabe, $hash)) { /* seguro */ }
// Buscar una subcadena
if (strpos($text, 'abc') !== false) { /* korrekt: auch Treffer an Position 0 */ }
Según el manual, hash_equals() espera el secreto conocido como primer parámetro y el valor del usuario como segundo, y compara en tiempo constante. Cómo generar tokens con seguridad lo describe Números aleatorios y tokens seguros en PHP.
declare(strict_types=1) no hace estrictas las comparaciones. La instrucción solo afecta a la conversión de tipos en las llamadas a funciones y en los valores de retorno. Las comparaciones laxas debe sustituirlas usted mismo en el código.
Las comparaciones estrictas tienen un reverso: fallan cuando los tipos no encajan. '42' === 42 es falso. Los valores de formularios y URL llegan a PHP como cadenas; los valores de la base de datos, según el controlador y la configuración, como cadenas o números. Quien introduce === en todas partes sin igualar los tipos genera errores que en la operación se notan como rechazo de usuarios legítimos.
La solución es una frontera clara: las entradas se validan en la recepción y se convierten al tipo correcto, por ejemplo con filter_var($wert, FILTER_VALIDATE_INT), y después en el código solo se tratan con declaraciones de tipo y comparaciones estrictas. Las declaraciones de tipo en parámetros, retornos y propiedades ayudan a ello, porque los tipos incorrectos se detectan pronto con un mensaje de error, en lugar de desaparecer en silencio dentro de una comparación.
Revisar sistemáticamente el código existente
En proyectos que han ido creciendo, las comparaciones laxas se cuentan por cientos. No todas son un problema. Concéntrese en los puntos en los que se decide sobre el acceso:
# Comparaciones laxas en líneas con términos relevantes para la seguridad
grep -rnE "[^=!]==[^=]" src/ --include="*.php" | grep -iE "token|hash|pass|role|rolle|admin|auth|sign"
# in_array sin comparación estricta
grep -rnE "in_array\([^)]*\)" src/ --include="*.php" | grep -v ", true)"
Las herramientas de análisis estático como PHPStan o Psalm pueden notificar de forma permanente las comparaciones laxas si se desea. Así, los casos nuevos ni siquiera llegan a la rama principal. Los fundamentos sobre los tipos los describe El sistema de tipos de PHP; cómo llevar antes las entradas al tipo correcto lo muestra Validar y filtrar entradas en PHP.
Qué significa esto para los sistemas heredados
Las aplicaciones que aún se ejecutan en PHP 7 están afectadas por las antiguas reglas de comparación, más laxas. A ello se suma un segundo problema: para PHP 8.1 y anteriores, el proyecto PHP ya no ofrece actualizaciones de seguridad oficiales, tampoco frente a PHP2Shell. La situación la describe PHP 8.1 y anteriores: qué rige ahora.
En una migración a PHP 8 cambian algunos resultados de comparación. Suele ser una mejora, pero puede romper código que se apoyaba en el comportamiento antiguo. Planifique por ello pruebas que cubran específicamente las comparaciones con números, valores vacíos y null.
¿Cómo ayuda PHP SOS en concreto?
- Buscamos en su código comparaciones laxas en los puntos que deciden sobre el inicio de sesión, los permisos o los pagos.
- Las sustituimos por comparaciones estrictas, expresiones
matchy funciones de comprobación adecuadas. - Configuramos el análisis estático de modo que las nuevas comparaciones laxas se detecten en el build.
- Acompañamos las migraciones a versiones actuales de PHP con pruebas para las reglas de comparación modificadas.
Una revisión de su software la describe Auditoría de seguridad y prueba de penetración.
Preguntas frecuentes
¿Qué es el type juggling en PHP?
PHP convierte automáticamente valores de distintos tipos en algunas operaciones, por ejemplo una cadena en un número. En la comparación laxa con ==, esa conversión decide el resultado. Es cómodo, pero puede dar lugar a comparaciones verdaderas aunque los valores no parezcan iguales.
¿Ha resuelto PHP 8 el problema?
En parte. Desde PHP 8.0, un número se compara con una cadena no numérica como cadena, de modo que 0 == 'a' ya no es verdadero. Pero dos cadenas numéricas se siguen comparando como números, y las comparaciones con true, false y null siguen reglas propias.
¿Ayuda declare(strict_types=1) frente a las comparaciones laxas?
No. strict_types controla si los tipos escalares se convierten automáticamente en las llamadas a funciones y en los retornos. No tiene ninguna influencia sobre los operadores de comparación == y !=.
¿Cómo comparo correctamente contraseñas y tokens?
Las contraseñas se comprueban con password_verify, y los tokens y las firmas con hash_equals. Ambas comparan de forma exacta y, en el caso de hash_equals, en tiempo constante. Una comparación con == o strcmp no es adecuada para ello.