Situación actual: PHP2Shell. Dos vulnerabilidades críticas de PHP (CVSS 9.2) permiten ejecutar código malicioso sin autenticación. Parches desde el 21/10/2026.Qué hacer ahora →

Guardar contraseñas en PHP con seguridad: usar bien password_hash

Cómo guardan los desarrolladores las contraseñas en PHP con password_hash y password_verify, cómo elegir el algoritmo adecuado y cómo sustituir hashes MD5 o SHA-1 antiguos sin restablecer la contraseña de todos los usuarios.

Actualizado 6 min de lecturaDesarrollo seguro

En resumen: en PHP, las contraseñas se guardan con password_hash() y se comprueban con password_verify(). El valor predeterminado PASSWORD_DEFAULT usa actualmente bcrypt, desde PHP 8.4 con factor de coste 12; donde PHP esté compilado con Argon2, PASSWORD_ARGON2ID es una buena alternativa. Los hashes MD5 o SHA-1 antiguos se migran envolviéndolos de inmediato en password_hash y sustituyéndolos por un hash directo en el siguiente inicio de sesión.

Qué tiene que ver una fuga de la base de datos con sus hashes

Tras una intrusión en un servidor web, la base de datos es uno de los primeros objetivos. Quien, como es posible con PHP2Shell, puede ejecutar código en el servidor, lee también las credenciales de la aplicación y, con ellas, la tabla de cuentas de usuario. Que las contraseñas contenidas en ella resulten legibles al cabo de horas o de años no se decide el día del ataque, sino en la programación.

Sorprendentemente, muchas aplicaciones siguen guardando contraseñas con procedimientos que nunca se concibieron para ese fin: MD5, SHA-1, un SHA-256 simple o incluso cifrado reversible. Estos procedimientos son rápidos, y ese es justo el problema. Con ellos, los atacantes pueden probar muchísimos candidatos por segundo.

PHP incorpora desde hace tiempo una solución que calcula despacio, genera automáticamente un salt para cada contraseña y guarda el procedimiento y los parámetros en el propio hash. Solo hay que usarla de forma consecuente.

password_hash en PHP: ¿bcrypt o Argon2id?

password_hash() conoce varios algoritmos. La selección según el manual de PHP:

Constante Algoritmo Observación
PASSWORD_DEFAULT actualmente bcrypt Valor predeterminado, puede cambiar en versiones futuras
PASSWORD_BCRYPT bcrypt Factor de coste 12 por defecto desde PHP 8.4, antes 10; solo cuentan los primeros 72 bytes
PASSWORD_ARGON2I Argon2i Desde PHP 7.2, solo si PHP está compilado con soporte para Argon2
PASSWORD_ARGON2ID Argon2id Desde PHP 7.3, misma condición

OWASP recomienda Argon2id como primera opción, con al menos 19 MiB de memoria, dos iteraciones y un hilo, y bcrypt sobre todo para sistemas existentes. Para la mayoría de las aplicaciones PHP, la diferencia en la práctica es menor que la diferencia entre cualquiera de estos algoritmos y MD5. Es más importante usar alguno de ellos y ajustar los parámetros con regularidad.

Una breve prueba muestra si Argon2id está disponible:

<?php
var_dump(defined('PASSWORD_ARGON2ID'));
var_dump(password_algos());

Guardar y comprobar: un patrón completo

El ejemplo siguiente muestra el registro y el inicio de sesión con actualización automática de hashes obsoletos. La comprobación de longitud protege frente al límite de 72 bytes de bcrypt:

<?php
declare(strict_types=1);

const PASSWORT_ALGO = PASSWORD_DEFAULT;

function passwortSpeichern(string $passwort): string
{
    if (strlen($passwort) > 72) {
        throw new InvalidArgumentException('Passwort zu lang für das gewählte Verfahren.');
    }
    return password_hash($passwort, PASSWORT_ALGO);
}

function passwortPruefen(string $eingabe, string $gespeicherterHash, callable $hashAktualisieren): bool
{
    if (!password_verify($eingabe, $gespeicherterHash)) {
        return false;
    }
    if (password_needs_rehash($gespeicherterHash, PASSWORT_ALGO)) {
        $hashAktualisieren(password_hash($eingabe, PASSWORT_ALGO));
    }
    return true;
}

strlen() cuenta bytes, no caracteres. Aquí es lo deseado, porque el límite de bcrypt se refiere a bytes. Según el manual, la columna de la base de datos debería admitir más de 60 caracteres; 255 es una buena elección. No establezca nunca un salt propio: la opción está obsoleta y se ignora desde PHP 8.0.

Con el cambio a PHP 8.4, el factor de coste predeterminado de bcrypt sube de 10 a 12. Gracias a password_needs_rehash(), los hashes existentes se actualizan automáticamente en el siguiente inicio de sesión. En sistemas muy cargados, conviene hacer antes de la migración una breve prueba de carga del inicio de sesión.

Consejo

Mida el factor de coste en su servidor de producción. Una comprobación individual debería consumir un tiempo de cálculo perceptible, pero quedar claramente por debajo de un segundo, para que los inicios de sesión no se conviertan en un cuello de botella bajo carga.

Sustituir hashes MD5 y SHA-1 antiguos

El verdadero desafío suele ser lo ya existente. Usted no conoce las contraseñas, así que no puede simplemente volver a aplicarles un hash. Esperar a que todos los usuarios vuelvan a iniciar sesión deja desprotegidas las cuentas inactivas. Por eso OWASP describe un procedimiento en dos etapas:

  1. Envolver de inmediato: envolver con password_hash() mediante un script cada valor MD5 o SHA-1 existente y guardarlo con una marca, por ejemplo en una columna hash_typ.
  2. Comprobar en el inicio de sesión: para las cuentas marcadas, convertir primero la entrada con el algoritmo antiguo y comprobarla después con password_verify() frente al valor envuelto.
  3. Sustituir directamente: tras un inicio de sesión correcto, generar un hash normal a partir del texto en claro y quitar la marca.
  4. Depurar el remanente: poner en restablecimiento de contraseña las cuentas que, pasado un plazo establecido, aún tengan hashes envueltos.
<?php
// Etapa 1 (una sola vez mediante script): $neu = password_hash($alterMd5Wert, PASSWORD_DEFAULT);
// Etapas 2 y 3 en el inicio de sesión:
if ($konto['hash_typ'] === 'md5-umhuellt') {
    $ok = password_verify(md5($eingabe), $konto['passwort_hash']);
    if ($ok) {
        $speichere(password_hash($eingabe, PASSWORD_DEFAULT), 'direkt');
    }
}

Los hashes envueltos son mejores que los valores MD5 desnudos, pero más débiles que los hashes directos. Son una solución de transición, no un objetivo. Tras un incidente en el que la base de datos pudiera haberse visto afectada, esta migración no sustituye la petición a los usuarios de que cambien sus contraseñas. Qué indicios apuntan a un incidente así lo describe ¿Ya estoy comprometido?.

Pepper, cifrado y otros caminos equivocados

Algunas ideas suenan a más seguridad, pero aportan poco o perjudican:

  • Construcciones de hash propias, como varios SHA-256 seguidos con un salt generado por uno mismo, son más rápidas que bcrypt y, por tanto, más débiles.
  • Las contraseñas cifradas son reversibles. Si la clave está en el mismo servidor, el cifrado apenas protege tras una intrusión.
  • Un pepper, es decir, un valor secreto adicional para todas las contraseñas, puede ser una capa extra si se guarda separado de la base de datos. Pero no sustituye a un buen algoritmo de hash y dificulta cambios posteriores.
  • El pre-hash antes de bcrypt, para sortear el límite de 72 bytes, entraña según OWASP riesgos propios. Un límite de longitud claro o Argon2id es más sencillo.

También en las reglas para contraseñas nuevas merece la pena fijarse en el estado de la técnica: la longitud protege más que las exigencias complicadas sobre caracteres especiales, y deberían rechazarse las contraseñas muy comunes o que ya han aparecido en filtraciones de datos. Los cambios forzados a intervalos fijos y sin motivo llevan, según la experiencia, más a patrones predecibles que a contraseñas más fuertes.

En los sistemas de inicio de sesión, las contraseñas tampoco deberían estar nunca solas: los métodos de dos factores para administradores y un freno frente a los intentos masivos de acceso complementan el almacenamiento. Ambos temas los trata la guía Proteger los inicios de sesión de administración frente a la fuerza bruta. Para enlaces de restablecimiento y códigos de un solo uso rigen otras reglas; véase Números aleatorios y tokens seguros en PHP. Una visión general de otras funciones la ofrece Funciones de seguridad integradas de PHP.

¿Cómo ayuda PHP SOS en concreto?

  • Comprobamos cómo guardan las contraseñas su aplicación y sus plugins y encontramos algoritmos obsoletos también en funciones secundarias.
  • Planificamos la migración de lo existente con envoltura, migración en el inicio de sesión y plazo para las cuentas restantes.
  • Ajustamos el factor de coste o los parámetros de Argon2 a sus servidores.
  • Tras un incidente, ayudamos en la decisión sobre los restablecimientos de contraseña y en su aplicación técnica.

Ofrecemos la revisión de software PHP propio como auditoría de seguridad y prueba de penetración.

Preguntas frecuentes

¿Qué algoritmo usa password_hash con PASSWORD_DEFAULT?

Actualmente bcrypt. El manual de PHP advierte de que el valor predeterminado puede cambiar cuando se incorporen algoritmos más fuertes. Por eso la columna de la base de datos debería admitir más de 60 caracteres; el manual cita 255 como buena elección.

¿Debería cifrar las contraseñas en lugar de aplicarles un hash?

No. Las contraseñas cifradas pueden volver a leerse con la clave, y esa clave suele estar en el mismo servidor. Un hash de contraseña es de sentido único: la aplicación solo puede comprobar si una entrada coincide.

¿Qué factor de coste debería usarse con bcrypt?

Desde PHP 8.4 el valor predeterminado es 12; antes era 10. OWASP cita 10 como límite inferior y recomienda subir tanto como toleren sus servidores. Mida en su propio hardware cuánto tarda una comprobación.

¿Qué significa el límite de 72 bytes de bcrypt?

bcrypt solo tiene en cuenta los primeros 72 bytes de una contraseña; todo lo posterior se trunca. Con caracteres acentuados, un carácter suele ocupar dos bytes. Por eso, limite expresamente la longitud o use Argon2id si su PHP está compilado con él.

¿Deben los usuarios cambiar sus contraseñas tras una intrusión?

Si no puede descartarse que la base de datos haya sido exfiltrada, debería disponerlo. Los buenos hashes ralentizan mucho el descifrado, pero las contraseñas débiles siguen siendo vulnerables. Si existe además una obligación de notificación, aclárelo con su delegado de protección de datos.

Fuentes

Contacto inmediato

Cada hora cuenta. Escríbanos ahora.

Describa brevemente el sistema, la versión de PHP y lo que ha observado. Nos pondremos en contacto, aclararemos la situación y trabajaremos en remoto hasta que el incidente quede cerrado y documentado.

Ayuda inmediata