"¿Cómo debería guardar exactamente las contraseñas de los usuarios en la base de datos?" — todo desarrollador se topa con esta pregunta una vez. La respuesta es clara. Aquí va la forma segura de guardarlas, por orden, sin pasos de ataque.
Por qué el texto plano, el cifrado y los hashes simples fallan todos
Asume que la base de datos se filtrará algún día. Cuando ocurra, el daño difiere enormemente según el método de almacenamiento.
- Texto plano: toda contraseña queda expuesta en el instante en que se filtra. Peor aún, la reutilización de contraseñas encadena la brecha hacia los otros servicios del usuario. El peor caso.
- Cifrado (reversible): la clave lo revierte — así que si la clave se filtra junto con ello, vuelves al texto plano. Una contraseña nunca necesita leerse de vuelta, así que ser reversible no te aporta nada.
- Hash simple (MD5/SHA-256): un hash rápido permite a un atacante probar conjeturas a gran velocidad. Las contraseñas comunes caen ante las tablas rainbow y la fuerza bruta.
Cuatro etapas hacia un almacenamiento seguro
Lo más rápido es entenderlo como ir añadiendo una corrección a la vez a un método débil.
La idea clave: un salt y un hash lento hacen trabajos distintos. Un salt derrota la precomputación (tablas rainbow) y el descifrado masivo por reutilización. Un hash lento reduce la fuerza bruta a un ritmo impracticable. Necesitas ambos antes de que sea seguro (→ qué es el hashing).
En la práctica: qué hacer ahora
Usa Argon2id (o bcrypt)
Para sistemas nuevos, haz de Argon2id la primera opción — también puede exigir memoria, lo que resiste la fuerza bruta por GPU. Si prefieres una implementación ampliamente probada, bcrypt es de sobra suficiente en la práctica. Usa la implementación de la biblioteca estándar tal cual para cualquiera de las dos.
Deja que la biblioteca genere el salt
bcrypt/Argon2 generan un salt por usuario y lo incrustan dentro de la cadena del hash. No mantienes una columna de salt aparte. Construir a mano MD5(salt + contraseña) es el clásico error que no hay que cometer.
Ajusta el coste para tu entorno
Fija el factor de coste de bcrypt, o la memoria/iteraciones/paralelismo de Argon2, tan alto como puedas mientras un inicio de sesión legítimo siga sintiéndose instantáneo. A medida que los servidores se vuelven más rápidos, también los atacantes — revísalo y súbelo cada año.
Verifica con la función de comparación estándar
Al iniciar sesión, coteja la entrada con el hash almacenado usando la función de verificación de la biblioteca (la mayoría usa una comparación de tiempo constante, segura ante ataques de temporización). No escribas tu propia igualdad de cadenas.
Migra los hashes débiles re-hasheando al iniciar sesión
Si ya guardas MD5/SHA-256, en el momento en que un usuario inicia sesión correctamente, re-hashea con el nuevo esquema y guárdalo. Para los usuarios que no han iniciado sesión, puedes reforzarlos mientras tanto volviendo a aplicar bcrypt sobre el hash existente (un hash en capas).
Errores comunes vs. la implementación correcta
Error común
- Cifrar las contraseñas para guardarlas (la clave se filtra → se acabó)
- Guardar
MD5(contraseña)oSHA-256(contraseña)directamente - Usar un único salt compartido para todos los usuarios
- Construir a mano
hash(salt + contraseña)
Implementación correcta
- Hash de un solo sentido con Argon2id / bcrypt
- Un salt por usuario (la biblioteca lo añade automáticamente)
- Coste tan alto como resulte aceptable, subido periódicamente
- Usar las funciones estándar de hash/verificación tal cual
Hash frente a cifrado frente a codificación
Estos tres se confunden, pero se diferencian por completo en si se pueden revertir. Solo el hash es aceptable para guardar contraseñas.
| Hash | Cifrado | Codificación (Base64, etc.) | |
|---|---|---|---|
| ¿Reversible? | No (un solo sentido) | Sí, con la clave | Sí, cualquiera |
| Clave | Ninguna | Necesaria | Ninguna |
| Uso típico | Guardar contraseñas, detectar manipulaciones | Mantener confidenciales los datos o el tráfico | Cambiar el formato de los datos |
| Para guardar contraseñas | Úsalo (salt + hash lento) | No | No (equivale al texto plano) |
Ajustes recomendados (OWASP)
Los ajustes mínimos de la OWASP Password Storage Cheat Sheet. Tómalos como un suelo y súbelos según tu servidor.
| Algoritmo | Ajuste mínimo | Cuándo usarlo |
|---|---|---|
| Argon2id | 19 MiB de memoria, 2 iteraciones, paralelismo 1 | Primera opción para sistemas nuevos |
| scrypt | N=2^17 (128 MiB), r=8, p=1 | Cuando Argon2id no está disponible |
| bcrypt | Coste 10 o más. Entrada limitada a 72 bytes | Sistemas existentes, trayectoria probada |
| PBKDF2-HMAC-SHA256 | 600.000 iteraciones o más | Cuando se exige cumplir FIPS 140 |
Revisa también lo que trae tu plataforma. password_hash() de PHP usa bcrypt por defecto (PASSWORD_DEFAULT), y PHP 8.4 subió el coste por defecto de 10 a 12. Django usa PBKDF2 por defecto; instala django[argon2] y pon Argon2 el primero en PASSWORD_HASHERS para cambiar. Django vuelve a aplicar el hash automáticamente a las contraseñas guardadas con un algoritmo antiguo cuando el usuario inicia sesión.
Cómo comprobar tu propio servicio
Normalmente puedes saber el esquema por el comienzo de la cadena del hash guardada. Mira unas pocas filas en un entorno que controles, sin copiar datos de producción a otro sitio.
| Aspecto del hash | Esquema | Qué hacer |
|---|---|---|
$argon2id$v=19$m=…,t=…,p=…$ | Argon2id | Comprueba que m, t y p cumplen el mínimo de OWASP |
$2y$12$… / $2b$10$… | bcrypt (el número es el coste) | Súbelo si está por debajo de 10 |
pbkdf2_sha256$1000000$… | PBKDF2 (el número es el número de iteraciones) | Comprueba que es al menos 600.000 |
| 32 caracteres hexadecimales | Muy probablemente MD5 | Migra re-hasheando al iniciar sesión |
| 40 / 64 caracteres hexadecimales | Muy probablemente SHA-1 / SHA-256 | Igual que arriba |
| La cadena de inicio de sesión tal cual | Texto plano | Migra primero. Evalúa el impacto si se filtrara |
Comprueba también lo siguiente:
- Las contraseñas introducidas no quedan registradas en los registros de la app o del servidor web, en los informes de errores ni en las herramientas de analítica
- Si usas bcrypt, has decidido cómo tratar las entradas de más de 72 bytes
- Las copias de seguridad y las copias de la base de datos hechas para pruebas solo contienen el mismo tipo de hashes
Ejemplo real: guardar en texto plano amplió el daño
En el incidente de Takufile-bin (2019), un acceso no autorizado a un servidor filtró unos 4,8 millones de registros, y las contraseñas de inicio de sesión que había entre ellos estaban guardadas en texto plano. Si se hubieran guardado con un hash lento con salt, el riesgo de que las contraseñas filtradas se usaran directamente en otros servicios habría sido mucho menor. Aunque no puedas evitar toda intrusión, cómo guardas las contraseñas cambia cuánto daño hace una filtración.
La visión de este sitio: para guardar contraseñas, usa la implementación estándar probada
El almacenamiento de contraseñas no es un sitio para ser creativo. Usar las implementaciones estándar de Argon2/bcrypt, ampliamente probadas, es lo más seguro y lo más fácil de operar. Nuestra postura: no innoves aquí. Gasta tu esfuerzo, en cambio, en reducir la dependencia de las contraseñas en general — una autenticación multifactor (MFA) exhaustiva, y un eventual paso a passkeys (sin contraseña). Ni siquiera el hash más fuerte puede salvar una contraseña débil y reutilizada.
Sigue leyendo
- Glosario: qué es el hashing de contraseñas / qué es un salt
- Fundamentos: guardar contraseñas de la forma correcta (lado del usuario) / elegir un gestor de contraseñas
- Defensa: elegir el MFA de la forma correcta (reduce la dependencia de las contraseñas)
- Glosario: qué es una passkey (avanzar hacia lo sin contraseña)
- Caso: la filtración de datos de Takufile-bin (2019) (almacenamiento de contraseñas en texto plano en la práctica)
Fuentes
- OWASP, "Password Storage Cheat Sheet": cheatsheetseries.owasp.org
- NIST, "SP 800-63B-4" (agosto de 2025), almacenamiento de contraseñas (salt y hash): pages.nist.gov
- RFC 9106, "Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications": rfc-editor.org
- Manual de PHP, "password_hash": php.net
- Documentación de Django, "Password management in Django" (5.2): docs.djangoproject.com