Una de las responsabilidades más importantes de cualquier desarrollador que construye un sistema con cuentas de usuario es gestionar las contraseñas de forma segura. Las brechas de seguridad que exponen contraseñas en texto plano han arruinado empresas y comprometido millones de cuentas. Y lo más llamativo es que la solución correcta no es especialmente complicada: requiere entender bien el hashing y aplicar las herramientas adecuadas.
En esta guía aprenderás qué es el hashing, por qué no todos los algoritmos son iguales para proteger contraseñas, cómo funciona el salt, y cómo implementar todo esto correctamente en Python, PHP, Node.js y Java.
Qué es el hashing
Una función hash toma una entrada de cualquier tamaño y produce una salida de tamaño fijo llamada hash o digest. Es una función de un solo sentido: a partir del hash no puedes recuperar la entrada original. Esto la hace perfecta para almacenar contraseñas: guardas el hash, no la contraseña.
# Ejemplo conceptual de cómo funciona el hashing
import hashlib
entrada = "mi_contraseña_secreta"
hash_resultado = hashlib.sha256(entrada.encode()).hexdigest()
print(hash_resultado)
# e3b0c44298fc1c149afbf4c8996fb924... (64 caracteres hexadecimales siempre)
# Propiedades fundamentales:
# 1. Determinista: la misma entrada siempre produce el mismo hash
# 2. Unidireccional: del hash no puedes obtener la entrada
# 3. Avalancha: un cambio mínimo en la entrada produce un hash completamente diferente
# 4. Tamaño fijo: independientemente del tamaño de la entrada
print(hashlib.sha256("a".encode()).hexdigest())
# ca978112ca1bbdcafac231b39a23dc4da786eff8147c4e72b9807785afee48bb
print(hashlib.sha256("b".encode()).hexdigest())
# 3e23e8160039594a33894f6564e1b1348bbd7a0088d42c4acb73eeaed59c009d
# Completamente diferente aunque solo cambió una letraCuando un usuario se registra, almacenas el hash de su contraseña. Cuando inicia sesión, calculas el hash de lo que escribe y lo comparas con el hash almacenado. Si coinciden, la contraseña es correcta, sin necesidad de guardar ni transmitir la contraseña real en ningún momento.
Por qué MD5 y SHA-1 no sirven para contraseñas
MD5 y SHA-1 son algoritmos de hashing diseñados para verificar la integridad de archivos y datos, no para proteger contraseñas. Usarlos para contraseñas es un error grave y muy común.
El problema es la velocidad. MD5 puede calcular miles de millones de hashes por segundo en hardware moderno con GPUs. Esto significa que un atacante que obtenga una base de datos con hashes MD5 puede probar miles de millones de contraseñas candidatas por segundo hasta encontrar la original.
# Velocidades aproximadas en hardware moderno (GPU potente):
# MD5: ~50.000 millones de hashes/segundo
# SHA-1: ~20.000 millones de hashes/segundo
# SHA-256: ~8.000 millones de hashes/segundo
# bcrypt (coste 12): ~10.000 hashes/segundo ← esto es lo que queremos
# Con MD5, un atacante puede probar TODAS las contraseñas de hasta 8 caracteres
# (minúsculas + números) en cuestión de minutos.
# Con bcrypt coste 12, probar esas mismas contraseñas llevaría años.
Además, MD5 y SHA-1 tienen colisiones conocidas: es posible encontrar dos entradas distintas que producen el mismo hash. Esto los hace inapropiados incluso para verificación de integridad en contextos de seguridad.
El problema de las rainbow tables
Una rainbow table es una base de datos precalculada que mapea hashes a sus contraseñas originales. Los atacantes las construyen offline una sola vez y luego las usan para descifrar hashes al instante, sin necesidad de calcular nada en el momento del ataque.
# Así funciona una rainbow table (simplificado):
# hash("password") = "5f4dcc3b5aa765d61d8327deb882cf99"
# hash("123456") = "e10adc3949ba59abbe56e057f20f883e"
# hash("qwerty") = "d8578edf8458ce06fbc5bb76a58c5ca4"
# hash("letmein") = "0d107d09f5bbe40cade3de5c71e9e9b7"
# Si la base de datos de tu aplicación se filtra y las contraseñas
# son MD5 sin sal, el atacante consulta la rainbow table y obtiene
# las contraseñas originales en milisegundos.
# Sitios como crackstation.net tienen rainbow tables con billones de entradas.
# Busca "5f4dcc3b5aa765d61d8327deb882cf99" y en segundos verás "password".
El salt: la solución a las rainbow tables
Un salt es una cadena aleatoria que se genera de forma única para cada usuario y se añade a la contraseña antes de calcular el hash. Hace que dos usuarios con la misma contraseña tengan hashes completamente diferentes, y hace que las rainbow tables precalculadas sean inútiles.
import os
import hashlib
# Sin salt: misma contraseña → mismo hash (vulnerable a rainbow tables)
hash1 = hashlib.sha256("password123".encode()).hexdigest()
hash2 = hashlib.sha256("password123".encode()).hexdigest()
print(hash1 == hash2) # True → el atacante solo necesita crackear una vez
# Con salt: misma contraseña → hashes completamente distintos
salt1 = os.urandom(32) # 32 bytes aleatorios únicos para el usuario 1
salt2 = os.urandom(32) # 32 bytes aleatorios únicos para el usuario 2
hash_con_salt1 = hashlib.sha256(salt1 + "password123".encode()).hexdigest()
hash_con_salt2 = hashlib.sha256(salt2 + "password123".encode()).hexdigest()
print(hash_con_salt1 == hash_con_salt2) # False → hashes completamente distintos
# El atacante tiene que atacar cada hash individualmente, multiplica el trabajo por N usuarios
El salt no es secreto: se almacena junto al hash en la base de datos. Su valor no es la privacidad sino la unicidad: garantiza que cada contraseña tiene un hash único aunque sea igual a la de otro usuario.
# Almacenamiento correcto: guardar el salt junto al hash
# Formato típico: $algoritmo$coste$salt$hash
# Todo en un único string que puedes guardar en una sola columna VARCHAR(255)
# bcrypt ya lo hace automáticamente:
# $2b$12$R9h/cIPz0gi.URNNX3kh2OPST9/PgBkqquzi.Ss7KIUgO2t0jWMUW
# $2b$ = algoritmo bcrypt
# 12 = factor de coste (2^12 = 4096 iteraciones)
# R9h/cIPz0gi.URNNX3kh2O = el salt (22 caracteres)
# PST9/PgBkqquzi.Ss7KIUgO2t0jWMUW = el hash (31 caracteres)
Algoritmos diseñados para contraseñas: bcrypt, Argon2 y scrypt
A diferencia de SHA-256 o MD5, estos algoritmos fueron diseñados específicamente para ser lentos y costosos computacionalmente. Su lentitud es una característica, no un defecto: hace que los ataques de fuerza bruta sean imprácticos incluso con hardware potente.
bcrypt
Publicado en 1999, bcrypt sigue siendo la recomendación más segura y ampliamente soportada para la mayoría de aplicaciones. Su factor de coste es configurable: aumentarlo hace el hash más lento, lo que puedes ajustar a medida que el hardware se vuelve más rápido con el tiempo.
# Python: instalar con pip install bcrypt
import bcrypt
# REGISTRAR USUARIO: generar hash
def hashear_password(password: str) -> bytes:
password_bytes = password.encode('utf-8')
# gensalt(rounds=12) genera un salt aleatorio con factor de coste 12
# rounds=12 significa 2^12 = 4096 iteraciones (recomendado en 2026)
# rounds=10 es el mínimo aceptable, rounds=14 para datos muy sensibles
salt = bcrypt.gensalt(rounds=12)
hash_resultado = bcrypt.hashpw(password_bytes, salt)
return hash_resultado # bytes, guardar en BD como BINARY(60) o VARCHAR(60)
# VERIFICAR LOGIN: comparar password con hash almacenado
def verificar_password(password: str, hash_almacenado: bytes) -> bool:
password_bytes = password.encode('utf-8')
return bcrypt.checkpw(password_bytes, hash_almacenado)
# Uso en registro
hash_guardado = hashear_password("mi_contraseña_segura")
print(hash_guardado)
# b'$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy'
# Uso en login
es_correcto = verificar_password("mi_contraseña_segura", hash_guardado)
print(es_correcto) # True
es_correcto = verificar_password("contraseña_incorrecta", hash_guardado)
print(es_correcto) # False
Argon2
Ganador de la Password Hashing Competition en 2015, Argon2 es el algoritmo más moderno y recomendado actualmente por OWASP y NIST. Tiene tres variantes: Argon2id (la recomendada para la mayoría de casos, combina protección contra ataques de tiempo lateral y de GPU), Argon2i (para hashing de contraseñas) y Argon2d (para criptomonedas).
# Python: pip install argon2-cffi
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError, VerificationError
# Configuración recomendada por OWASP para Argon2id
ph = PasswordHasher(
time_cost=2, # número de iteraciones
memory_cost=65536, # memoria en KB (64 MB)
parallelism=1, # hilos paralelos
hash_len=32, # longitud del hash en bytes
salt_len=16 # longitud del salt en bytes
)
# Registrar usuario
def hashear_argon2(password: str) -> str:
return ph.hash(password) # incluye salt, parámetros y hash en un único string
# Verificar login
def verificar_argon2(password: str, hash_almacenado: str) -> bool:
try:
return ph.verify(hash_almacenado, password)
except (VerifyMismatchError, VerificationError):
return False
# Comprobar si el hash necesita actualizarse (parámetros obsoletos)
def necesita_rehash(hash_almacenado: str) -> bool:
return ph.check_needs_rehash(hash_almacenado)
# Uso
hash_argon2 = hashear_argon2("mi_contraseña")
print(hash_argon2)
# $argon2id$v=19$m=65536,t=2,p=1$abc123...salt...$hash...
# En cada login exitoso, comprobar si hay que actualizar el hash
def login(email: str, password: str):
usuario = db.buscar_por_email(email)
if not usuario:
return False
if not verificar_argon2(password, usuario.password_hash):
return False
# Si los parámetros de hash son obsoletos, regenerar con los actuales
if necesita_rehash(usuario.password_hash):
nuevo_hash = hashear_argon2(password)
db.actualizar_hash(usuario.id, nuevo_hash)
return True
scrypt
scrypt está diseñado para ser costoso tanto en CPU como en memoria, lo que lo hace resistente a ataques con hardware especializado (ASICs y FPGAs). Está disponible en la librería estándar de Python desde la versión 3.6.
import hashlib
import os
import base64
def hashear_scrypt(password: str) -> str:
salt = os.urandom(32)
# Parámetros recomendados por OWASP:
# n=2^17 (131072): factor de coste de CPU/memoria
# r=8: tamaño del bloque
# p=1: factor de paralelización
hash_bytes = hashlib.scrypt(
password.encode('utf-8'),
salt=salt,
n=131072,
r=8,
p=1,
dklen=32 # longitud del hash derivado en bytes
)
# Codificar salt y hash juntos para almacenarlos en una sola columna
salt_b64 = base64.b64encode(salt).decode('utf-8')
hash_b64 = base64.b64encode(hash_bytes).decode('utf-8')
return f"scrypt${salt_b64}${hash_b64}"
def verificar_scrypt(password: str, hash_almacenado: str) -> bool:
_, salt_b64, hash_b64 = hash_almacenado.split('$')
salt = base64.b64decode(salt_b64)
hash_real = base64.b64decode(hash_b64)
hash_intento = hashlib.scrypt(
password.encode('utf-8'),
salt=salt,
n=131072,
r=8,
p=1,
dklen=32
)
# Comparación segura: evita timing attacks
return hmac.compare_digest(hash_intento, hash_real)
Implementación completa por lenguaje
PHP: password_hash y password_verify
PHP tiene las funciones más sencillas del ecosistema para esto. Desde PHP 5.5, las funciones nativas password_hash() y password_verify() hacen todo el trabajo automáticamente.
<?php
// REGISTRAR: generar hash bcrypt automáticamente
function hashearPassword(string $password): string {
// PASSWORD_BCRYPT con coste 12 (por defecto es 10)
// PASSWORD_ARGON2ID si PHP >= 7.3 (recomendado)
return password_hash($password, PASSWORD_ARGON2ID, [
'memory_cost' => 65536, // 64 MB
'time_cost' => 4,
'threads' => 1,
]);
// Si no hay soporte para Argon2:
// return password_hash($password, PASSWORD_BCRYPT, ['cost' => 12]);
}
// VERIFICAR LOGIN
function verificarPassword(string $password, string $hashAlmacenado): bool {
return password_verify($password, $hashAlmacenado);
}
// COMPROBAR SI EL HASH NECESITA ACTUALIZACIÓN
function necesitaRehash(string $hashAlmacenado): bool {
return password_needs_rehash($hashAlmacenado, PASSWORD_ARGON2ID, [
'memory_cost' => 65536,
'time_cost' => 4,
'threads' => 1,
]);
}
// Uso en registro
$hash = hashearPassword("mi_contraseña_segura");
echo $hash;
// $argon2id$v=19$m=65536,t=4,p=1$abc...salt...$hash...
// Uso en login
$esValido = verificarPassword("mi_contraseña_secreta", $hash);
var_dump($esValido); // bool(true) o bool(false)
// Nunca hagas esto (comparación directa de strings):
// if ($hash === password_hash($password, PASSWORD_BCRYPT)) { ... } // MAL
// Usa siempre password_verify() que hace comparación segura contra timing attacks
?>
Node.js: bcrypt y argon2
// npm install bcrypt argon2
// Opción 1: bcrypt
const bcrypt = require('bcrypt');
const SALT_ROUNDS = 12; // factor de coste
// Registrar usuario
async function hashearPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
// bcrypt genera el salt automáticamente e incluye todo en el hash resultante
}
// Verificar login
async function verificarPassword(password, hashAlmacenado) {
return bcrypt.compare(password, hashAlmacenado);
// Comparación segura contra timing attacks incluida
}
// Uso
const hash = await hashearPassword("mi_contraseña");
console.log(hash);
// $2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy
const esValido = await verificarPassword("mi_contraseña", hash);
console.log(esValido); // true
// Opción 2: argon2 (recomendada si el entorno lo soporta)
const argon2 = require('argon2');
async function hashearArgon2(password) {
return argon2.hash(password, {
type: argon2.argon2id,
memoryCost: 65536, // 64 MB
timeCost: 2,
parallelism: 1,
});
}
async function verificarArgon2(password, hashAlmacenado) {
try {
return await argon2.verify(hashAlmacenado, password);
} catch {
return false;
}
}
// Controlador de login completo en Express
app.post('/login', async (req, res) => {
const { email, password } = req.body;
if (!email || !password) {
return res.status(400).json({ error: 'Email y contraseña requeridos' });
}
const usuario = await db.usuarios.findOne({ where: { email } });
// Siempre hacer la comparación, aunque el usuario no exista
// para evitar timing attacks por enumeración de usuarios
const hashDummy = '$2b$12$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZdL17lhWy';
const hashAComparar = usuario ? usuario.password_hash : hashDummy;
const esValido = await bcrypt.compare(password, hashAComparar) && usuario !== null;
if (!esValido) {
return res.status(401).json({ error: 'Credenciales incorrectas' });
}
// Generar token de sesión...
res.json({ token: generarJWT(usuario) });
});
Java: Spring Security
// Con Spring Security: BCryptPasswordEncoder
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.argon2.Argon2PasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.context.annotation.Bean;
// Configuración como Bean de Spring
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt con coste 12
return new BCryptPasswordEncoder(12);
// O Argon2 (recomendado):
// return new Argon2PasswordEncoder(16, 32, 1, 65536, 2);
}
}
// Servicio de usuarios
@Service
public class UsuarioService {
private final PasswordEncoder passwordEncoder;
private final UsuarioRepository repositorio;
public UsuarioService(PasswordEncoder passwordEncoder, UsuarioRepository repositorio) {
this.passwordEncoder = passwordEncoder;
this.repositorio = repositorio;
}
// Registrar usuario
public Usuario registrar(String email, String password) {
// Validar fortaleza de la contraseña antes de hashear
validarFortaleza(password);
String hash = passwordEncoder.encode(password);
Usuario usuario = new Usuario();
usuario.setEmail(email);
usuario.setPasswordHash(hash);
return repositorio.save(usuario);
}
// Verificar login
public boolean verificarLogin(String password, String hashAlmacenado) {
return passwordEncoder.matches(password, hashAlmacenado);
}
private void validarFortaleza(String password) {
if (password.length() < 8) {
throw new IllegalArgumentException("La contraseña debe tener al menos 8 caracteres");
}
// Añadir más validaciones según política de seguridad
}
}
Timing attacks: la comparación segura
Un timing attack explota las diferencias en el tiempo que tarda una comparación de strings dependiendo de cuántos caracteres coinciden. Si comparas hashes con ==, un atacante puede medir los tiempos de respuesta y deducir si está acercándose al hash correcto.
# ❌ VULNERABLE: comparación normal termina en cuanto encuentra una diferencia
def verificar_hash_inseguro(hash_introducido, hash_real):
return hash_introducido == hash_real # el tiempo varía según cuántos chars coinciden
# ✅ SEGURO: comparación en tiempo constante (siempre tarda lo mismo)
import hmac
def verificar_hash_seguro(hash_introducido: str, hash_real: str) -> bool:
# hmac.compare_digest compara byte a byte en tiempo constante
# independientemente de dónde esté la primera diferencia
return hmac.compare_digest(
hash_introducido.encode('utf-8'),
hash_real.encode('utf-8')
)
# En la práctica, bcrypt.checkpw(), password_verify(), bcrypt.compare()
# y los métodos de Spring Security ya usan comparación en tiempo constante internamente.
# Nunca implementes la comparación de hashes de contraseña manualmente con ==.
Política de contraseñas: qué exigir y qué no
Las reglas de contraseña que impongas afectan tanto a la seguridad como a la experiencia de usuario. Las recomendaciones han cambiado significativamente: el NIST (Instituto Nacional de Estándares y Tecnología de EE.UU.) publicó en 2024 nuevas directrices que contradicen prácticas muy extendidas.
import re
# Recomendaciones NIST 2024: validación de contraseñas
def validar_password(password: str) -> tuple[bool, list[str]]:
errores = []
# ✅ SÍ: longitud mínima de 8 caracteres (NIST recomienda 15 para mayor seguridad)
if len(password) < 8:
errores.append("Debe tener al menos 8 caracteres")
# ✅ SÍ: longitud máxima de 64-256 caracteres (para evitar DoS con strings enormes)
if len(password) > 256:
errores.append("No puede superar los 256 caracteres")
# ✅ SÍ: comprobar contra lista de contraseñas comunes/filtradas
if es_contrasena_comun(password):
errores.append("Esta contraseña es demasiado común. Elige una más original.")
# ✅ SÍ: permitir cualquier carácter incluyendo espacios y emojis
# Una frase larga ("mi gato come pizza los viernes") es más segura que "P@ssw0rd!"
# ❌ NO: no obligar a mezclar mayúsculas + minúsculas + números + símbolos
# Esto lleva a contraseñas predecibles como "Password1!" que son más débiles de lo que parecen
# ❌ NO: no caducar contraseñas periódicamente sin motivo
# El NIST eliminó esta recomendación en 2017. Caducar contraseñas lleva a patrones débiles.
# Sí cambiar si hay evidencia de compromiso.
return len(errores) == 0, errores
# Comprobar contra Have I Been Pwned (API de contraseñas filtradas)
import hashlib
import requests
def es_contrasena_filtrada(password: str) -> bool:
"""
Usa la API k-anonymity de HIBP: solo envía los primeros 5 caracteres del hash SHA-1,
nunca la contraseña ni el hash completo.
"""
sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest().upper()
prefijo = sha1_hash[:5]
sufijo = sha1_hash[5:]
try:
respuesta = requests.get(
f"https://api.pwnedpasswords.com/range/{prefijo}",
timeout=3
)
hashes = respuesta.text.splitlines()
for linea in hashes:
hash_parcial, conteo = linea.split(':')
if hash_parcial == sufijo:
return True # contraseña encontrada en {conteo} brechas
except requests.RequestException:
pass # si la API no responde, no bloquear al usuario
return False
Migrar hashes existentes a un algoritmo más seguro
Si tienes una aplicación con contraseñas almacenadas en MD5 o SHA-1 sin salt, necesitas migrar a bcrypt o Argon2. El truco es que no puedes conocer las contraseñas originales (están hasheadas), pero puedes migrar de forma gradual: actualizas el hash cada vez que el usuario inicia sesión correctamente.
# Estrategia de migración progresiva sin resetear contraseñas
import hashlib
import bcrypt
def login_con_migracion(email: str, password: str) -> bool:
usuario = db.buscar_por_email(email)
if not usuario:
return False
# Fase 1: el usuario todavía tiene hash MD5 antiguo
if usuario.tipo_hash == 'md5':
md5_viejo = hashlib.md5(password.encode()).hexdigest()
if md5_viejo != usuario.password_hash:
return False # contraseña incorrecta
# Login correcto: migrar a bcrypt ahora que tenemos la contraseña en claro
nuevo_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
db.actualizar(usuario.id, {
'password_hash': nuevo_hash,
'tipo_hash': 'bcrypt'
})
return True
# Fase 2: el usuario ya tiene hash bcrypt moderno
elif usuario.tipo_hash == 'bcrypt':
return bcrypt.checkpw(password.encode('utf-8'), usuario.password_hash)
return False
# Con el tiempo, el porcentaje de usuarios con MD5 cae naturalmente
# hasta que puedes forzar un reset de contraseña para los que queden
Errores comunes que debes evitar
Guardar la contraseña en texto plano. El error más básico y el más grave. Si la base de datos se filtra, todas las cuentas quedan comprometidas inmediatamente. Jamás almacenes contraseñas sin hashear.
Usar MD5 o SHA-256 directamente para contraseñas. Son demasiado rápidos. Un atacante con una GPU puede probar miles de millones de combinaciones por segundo. Usa bcrypt, Argon2 o scrypt.
Generar el salt manualmente de forma incorrecta. Si usas bcrypt, Argon2 o las funciones nativas de cada lenguaje, el salt se genera automáticamente y correctamente. Si lo haces manualmente, usa siempre os.urandom() o el equivalente criptográfico de tu lenguaje, nunca random.random().
# ❌ MAL: usar el módulo random (no es criptográficamente seguro)
import random
import string
salt_inseguro = ''.join(random.choices(string.ascii_letters, k=16))
# ✅ BIEN: usar os.urandom() o secrets
import os
import secrets
salt_seguro = os.urandom(32)
salt_seguro2 = secrets.token_bytes(32)
Transmitir la contraseña en texto plano. Usa siempre HTTPS. El hashing protege las contraseñas en la base de datos, pero si las transmites por HTTP sin cifrar, un atacante en la red las intercepta antes de que lleguen a hashear.
Hashear la contraseña en el cliente con JavaScript. Si el cliente envía el hash en lugar de la contraseña, el hash se convierte efectivamente en la contraseña. Un atacante que obtiene el hash puede autenticarse sin conocer la contraseña original.
Usar un coste demasiado bajo en bcrypt. El coste por defecto de bcrypt en muchas librerías es 10. En 2026, con hardware moderno, el mínimo recomendado es 12. Comprueba que el tiempo de verificación sea de al menos 100-300 ms en tu servidor de producción.
Resumen
- El hashing convierte una contraseña en una cadena irreversible. Almacenas el hash, nunca la contraseña.
- MD5 y SHA-1/SHA-256 son demasiado rápidos para contraseñas: permiten millones de millones de intentos por segundo. No los uses.
- El salt es una cadena aleatoria única por usuario que se añade antes de hashear. Invalida las rainbow tables y hace que dos usuarios con la misma contraseña tengan hashes distintos.
- Usa bcrypt (coste mínimo 12), Argon2id (primera opción según OWASP 2024) o scrypt. Todos gestionan el salt automáticamente.
- Usa siempre las funciones de comparación segura de cada librería (
bcrypt.checkpw(),password_verify(),bcrypt.compare()). Nunca compares hashes con==. - Valida la fortaleza de las contraseñas en el servidor, no solo en el cliente. Comprueba contra listas de contraseñas filtradas con la API de HIBP.
- Si tienes hashes MD5 o SHA antiguo, migra de forma gradual actualizando el hash en cada login exitoso.
No hay comentarios todavía. Sé el primero en compartir tu opinión.