En A01 distinguimos autenticación ("¿quién eres?") de autorización ("¿qué puedes hacer?") y dijimos que la autenticación tendría su propia lección. Ha llegado. A07:2021 – Fallos de Identificación y Autenticación (Identification and Authentication Failures) —antes llamada "Pérdida de Autenticación"— agrupa todo lo que sale mal al verificar la identidad de un usuario y al mantener esa identidad durante la sesión: contraseñas débiles, fuerza bruta y credential stuffing, gestión de sesiones deficiente, ausencia de MFA, recuperación de cuenta insegura.

Conviene situarla entre sus vecinas: A07 valida quién eres (login, sesiones, MFA); A01 decide qué puedes hacer una vez identificado; y A02 se ocupa de cómo se almacenan las credenciales (bcrypt/argon2). Las tres colaboran, pero resuelven problemas distintos. En BazarNube revisaremos el login y la gestión de sesiones de la API Express, dos de los puntos más atacados de cualquier aplicación con cuentas de usuario.

Aviso legal y ético: los ejemplos de ataque (fuerza bruta, credential stuffing) son ilustrativos y con datos ficticios. Practica solo sobre sistemas propios o con autorización explícita.

Contenido

  1. Contraseñas débiles y políticas modernas
  2. Fuerza bruta y credential stuffing
  3. Mensajes de error y enumeración de usuarios
  4. Gestión de sesiones segura
  5. Autenticación multifactor (MFA)
  6. Recuperación de cuenta sin agujeros
  7. Errores comunes, ejercicios y soluciones

  1. Contraseñas débiles y políticas modernas

El registro de BazarNube aceptaba cualquier contraseña, incluidas las más comunes:

// register.js — VULNERABLE: sin politica de contrasenas
router.post('/api/register', async (req, res) => {
  const { email, password } = req.body;
  await createUser(email, password);   // acepta "123456", "password"...
  res.json({ ok: true });
});

Las recomendaciones actuales (alineadas con NIST y el OWASP Authentication Cheat Sheet) han cambiado respecto a las viejas reglas:

  • Longitud mínima razonable (p. ej. 12) y permitir contraseñas largas (soporta al menos 64 caracteres, sin truncar).
  • Comprobar contra listas de contraseñas comprometidas/comunes (rechazar "password", "bazarnube2024"...).
  • No imponer reglas de composición absurdas (mayús + símbolo obligatorios) ni caducidad periódica forzada: fomentan patrones predecibles. Mejor: longitud + comprobación de exposición.
  • Permitir gestores de contraseñas (no bloquear pegar).
// register.js — SEGURO: politica moderna
const zxcvbn = require('zxcvbn');               // estima la fortaleza real
router.post('/api/register', async (req, res) => {
  const { email, password } = req.body;
  if (password.length < 12) return res.status(400).json({ error: 'Minimo 12 caracteres' });
  if (zxcvbn(password).score < 3) return res.status(400).json({ error: 'Contrasena debil' });
  if (await isBreached(password)) return res.status(400).json({ error: 'Contrasena comprometida' });
  await createUser(email, password);            // se almacena con bcrypt/argon2 (ver A02)
  res.json({ ok: true });
});

isBreached consulta (de forma que preserve la privacidad, con k-anonymity) si la contraseña aparece en filtraciones conocidas. Recuerda: cómo se guarda la contraseña ya lo resolvimos en A02 con bcrypt/argon2.

  1. Fuerza bruta y credential stuffing

  • Fuerza bruta: probar muchas contraseñas contra una cuenta.
  • Credential stuffing: usar pares usuario/contraseña filtrados de otras brechas, apostando a que la gente reutiliza credenciales. Es hoy el ataque más común contra logins.

El login de BazarNube no tenía ningún freno:

// login.js — VULNERABLE: sin limite de intentos
router.post('/api/login', async (req, res) => {
  const u = await getUser(req.body.email);
  if (u && await verifyPassword(req.body.password, u.pwd)) {
    req.session.userId = u.id;
    return res.json({ ok: true });
  }
  res.status(401).json({ error: 'Credenciales invalidas' });
});

Sin límites, un atacante lanza miles de intentos por minuto. Defensas combinadas:

// login.js — SEGURO: rate limiting + bloqueo progresivo
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({ windowMs: 15*60*1000, max: 10 }); // por IP

router.post('/api/login', loginLimiter, async (req, res) => {
  const email = req.body.email;
  if (await isLockedOut(email)) return res.status(429).json({ error: 'Demasiados intentos' });
  const u = await getUser(email);
  if (u && await verifyPassword(req.body.password, u.pwd)) {
    await resetFailures(email);
    await regenerateSession(req);           // ver seccion 4 (fijacion de sesion)
    req.session.userId = u.id;
    return res.json({ ok: true });
  }
  await recordFailure(email);               // bloqueo progresivo por cuenta
  res.status(401).json({ error: 'Credenciales invalidas' });
});
Control Frente a
Rate limiting por IP Fuerza bruta desde una fuente
Bloqueo progresivo por cuenta Ataques dirigidos a un usuario
CAPTCHA tras N fallos Automatización masiva
MFA Credential stuffing (aunque acierten la clave)
Detección de logins anómalos Credential stuffing distribuido

El credential stuffing usa muchas IPs y una sola contraseña por cuenta, así que el límite por IP no basta: por eso MFA y la detección de patrones son claves.

  1. Mensajes de error y enumeración de usuarios

Un detalle sutil: si el login responde "este email no existe" frente a "contraseña incorrecta", regalas al atacante una forma de enumerar qué cuentas existen. Lo mismo con tiempos de respuesta distintos.

// VULNERABLE: revela si el email existe
if (!u) return res.status(404).json({ error: 'Usuario no encontrado' });
if (!ok) return res.status(401).json({ error: 'Contrasena incorrecta' });

// SEGURO: mensaje y comportamiento uniformes
// (misma respuesta exista o no el usuario; mismo coste de tiempo)
return res.status(401).json({ error: 'Credenciales invalidas' });

El mismo cuidado aplica al registro ("este email ya está registrado") y a la recuperación de contraseña (sección 6): responde siempre de forma uniforme.

  1. Gestión de sesiones segura

Una vez autenticado, la sesión es la que "recuerda" quién eres. Sus fallos son tan graves como los del login.

flowchart LR
  A[Login correcto] --> B[Regenerar id de sesion]
  B --> C[Cookie HttpOnly Secure SameSite]
  C --> D[Expiracion por inactividad y absoluta]
  D --> E[Logout invalida sesion en servidor]

Buenas prácticas:

  • Regenerar el id de sesión al iniciar sesión (regenerateSession): evita la fijación de sesión, donde el atacante fija un id conocido antes del login y lo hereda tras él.
  • Cookies con HttpOnly (no accesible por JS, mitiga XSS —ver 03-04), Secure (solo HTTPS) y SameSite (mitiga CSRF).
  • Expiración: por inactividad (p. ej. 30 min) y absoluta (p. ej. 12 h), tras la cual hay que reautenticarse.
  • Logout real: invalidar la sesión en el servidor, no solo borrar la cookie en el cliente.
  • Tokens de sesión aleatorios y de suficiente entropía (los genera el framework; no inventes los tuyos).
// config de sesion segura (Express)
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false, saveUninitialized: false,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30*60*1000 }
}));

function regenerateSession(req) {
  return new Promise((ok, err) => req.session.regenerate(e => e ? err(e) : ok()));
}

Si usas JWT en lugar de sesiones de servidor, cuida su propio conjunto de riesgos: firma verificada (algoritmo fijado, nunca alg: none), caducidad corta, y un mecanismo de revocación (los JWT no se invalidan solos al hacer logout).

  1. Autenticación multifactor (MFA)

La MFA añade un segundo factor (algo que tienes, como un TOTP o una passkey) al "algo que sabes" (la contraseña). Es la defensa más eficaz contra el credential stuffing: aunque el atacante tenga la contraseña correcta, sin el segundo factor no entra.

  • Ofrece MFA a todos los clientes de BazarNube y exígela a las cuentas de administración y a operaciones sensibles (cambios de pago, exportaciones).
  • Prefiere TOTP (apps de autenticación) o WebAuthn/passkeys (resistentes a phishing) frente a SMS, más vulnerable a SIM swapping.
  • Considera step-up authentication: pedir el segundo factor solo en acciones críticas, no en cada login.

  1. Recuperación de cuenta sin agujeros

El flujo de "he olvidado mi contraseña" es un punto débil clásico: si es inseguro, el atacante lo usa para tomar la cuenta sin conocer la contraseña.

  • Genera un token aleatorio de un solo uso y caducidad corta (p. ej. 15-30 min), guarda solo su hash, y envíalo por un canal verificado (email).
  • No reveles si el email existe: responde siempre "si la cuenta existe, te hemos enviado instrucciones".
  • Invalida el token tras usarlo y tras cambiar la contraseña; cierra las sesiones activas al restablecerla.
  • Nunca uses preguntas de seguridad adivinables ("nombre de tu mascota") como único factor de recuperación.
// recuperacion — patron seguro (resumen)
router.post('/api/forgot', async (req, res) => {
  const u = await getUser(req.body.email);
  if (u) {
    const token = crypto.randomBytes(32).toString('hex');
    await saveResetToken(u.id, sha256(token), Date.now() + 30*60*1000); // guarda hash + expiracion
    await sendEmail(u.email, resetLink(token));
  }
  res.json({ message: 'Si la cuenta existe, enviaremos instrucciones' }); // respuesta uniforme
});

Errores Comunes y Consejos

  • Login sin límites. Rate limiting + bloqueo progresivo son imprescindibles.
  • Mensajes que revelan si un usuario existe. Uniforma respuestas y tiempos (login, registro, recuperación).
  • No regenerar la sesión al entrar. Abre la puerta a la fijación de sesión.
  • Cookies sin HttpOnly/Secure/SameSite. Facilitas robo de sesión y CSRF.
  • No ofrecer MFA o dejarla opcional en cuentas de administración.
  • Recuperación con tokens largos, sin caducar o adivinables. Es una vía directa de toma de cuenta.
  • Confundir A07 con A02. A07 valida y mantiene la identidad; A02 almacena la credencial (bcrypt/argon2).
  • Consejo: apóyate en soluciones probadas (frameworks de sesión, proveedores de identidad) antes de construir autenticación desde cero.

Ejercicios

Ejercicio 1. Explica por qué el rate limiting por IP no detiene por sí solo un ataque de credential stuffing y qué controles lo complementan.

Ejercicio 2. Este login revela información. Identifica el problema y corrígelo:

const u = await getUser(email);
if (!u) return res.status(404).json({ error: 'Ese email no esta registrado' });
if (!await verify(pass, u.pwd)) return res.status(401).json({ error: 'Clave erronea' });

Ejercicio 3. ¿Qué es la fijación de sesión y qué única línea, bien colocada, la previene?

Soluciones

Solución 1. El credential stuffing se distribuye entre muchas IPs y prueba una contraseña (la filtrada) por cuenta, así que apenas dispara los límites por IP. Lo complementan: MFA (aunque acierten la clave, falta el segundo factor), detección de patrones anómalos (muchas cuentas distintas, agentes sospechosos), comprobación de contraseñas comprometidas al registrarse, y CAPTCHA/step-up ante señales de automatización.

Solución 2. Permite enumerar usuarios: distingue "email no registrado" (404) de "clave errónea" (401). Corrección: respuesta uniforme, mismo código y mensaje exista o no el usuario, y cuidando que el tiempo de respuesta no delate el caso (return res.status(401).json({ error: 'Credenciales invalidas' }) en ambos).

Solución 3. La fijación de sesión ocurre cuando el atacante consigue que la víctima use un id de sesión que el atacante ya conoce (p. ej. se lo fija por URL) y, tras el login, hereda esa sesión autenticada. Se previene regenerando el id de sesión justo al autenticarse (req.session.regenerate(...)), de modo que el id anterior queda inservible.

Conclusión

A07 protege la puerta de entrada y la permanencia del usuario: políticas de contraseña modernas (longitud + comprobación de exposición), frenos a la fuerza bruta y al credential stuffing (rate limiting, bloqueo, MFA), mensajes uniformes contra la enumeración, gestión de sesiones sólida (regeneración, cookies seguras, expiración, logout real) y recuperación de cuenta con tokens efímeros de un solo uso. Todo apoyándose en el almacenamiento seguro que ya montamos en A02.

Entrada de backlog — A07: política de contraseñas con longitud mínima y comprobación de contraseñas comprometidas; rate limiting + bloqueo progresivo en login; mensajes de login/registro uniformes; regeneración de sesión y cookies HttpOnly/Secure/SameSite; MFA obligatoria para administración; flujo de recuperación con token efímero hasheado.

Ya sabemos identificar y autenticar. Pero, ¿podemos confiar en la integridad del software y los datos que fluyen por BazarNube —actualizaciones, objetos serializados, artefactos del pipeline? La siguiente lección aborda A08:2021 – Fallos de Integridad de Software y Datos, incluyendo la deserialización insegura en el módulo Java.

Curso de OWASP: Directrices y Estándares para la Seguridad en Aplicaciones Web

Módulo 1: Introducción a OWASP

Módulo 2: Principales Proyectos de OWASP

Módulo 3: OWASP Top Ten 2021 en Profundidad

Módulo 4: OWASP ASVS (Application Security Verification Standard)

Módulo 5: OWASP SAMM (Software Assurance Maturity Model)

Módulo 6: OWASP ZAP (Zed Attack Proxy)

Módulo 7: Buenas Prácticas y Recomendaciones

Módulo 8: Ejercicios Prácticos y Casos de Estudio

Módulo 9: Evaluación y Certificación

© Copyright 2026. Todos los derechos reservados