Con la identidad verificada (07-01) y el canal cifrado (07-02), un atacante ya no puede leer el token de Ana ni hacerse pasar por Pedidos. Pero puede seguir siendo un usuario legítimo que envía una petición malintencionada: un id con caracteres extraños, un JSON de 50 MB, un filtro de MongoDB disfrazado de cadena, o simplemente GET /v1/pedidos/ped-88214 para ver si cuela. Esta lección trata de lo que cada servicio hace con lo que recibe y con lo que guarda: los principios que ordenan todo lo demás, el OWASP API Security Top 10 recorrido con ejemplos de TechCorp, validación y saneamiento de entrada con zod, las inyecciones (SQL, NoSQL, comandos, logs), cabeceras con helmet, dependencias, secretos en el código, protección de datos personales (RGPD a nivel práctico), auditoría, pruebas de seguridad y respuesta ante vulnerabilidades. Termina con la lista de comprobación que se incorpora a la plantilla techcorp/plantilla-servicio-node. Autenticación (07-01), TLS (07-02) y contenedores/Kubernetes (07-04) solo se enlazan.

Aviso. Las medidas de esta lección son las habituales en un equipo de desarrollo, no un programa de seguridad completo. La aplicación real del RGPD, de PCI DSS y de cualquier política de retención o auditoría debe revisarse con un profesional de seguridad y con el responsable de cumplimiento/protección de datos de la organización.

Contenido

  1. Principios que ordenan el resto
  2. OWASP API Security Top 10 (2023) con ejemplos de TechCorp
  3. Validación y saneamiento de entrada con zod
  4. Inyección: SQL, NoSQL, comandos y logs
  5. Cabeceras de seguridad con helmet
  6. Gestión de dependencias
  7. Secretos en el código: nunca, y cómo asegurarlo
  8. Protección de datos personales: RGPD en la práctica
  9. Auditoría de acciones sensibles
  10. Pruebas de seguridad: SAST, DAST, revisión y pentest
  11. Respuesta ante vulnerabilidades
  12. Lista de comprobación de seguridad por servicio

  1. Principios que ordenan el resto

Seis principios que se repiten en cada decisión de este módulo:

Principio Significado Ya aplicado en TechCorp
Mínimo privilegio Cada componente solo puede lo que necesita Usuarios svc_* por esquema (02-04), permisos de RabbitMQ por cola (07-02), scopes por cliente (07-01)
Defensa en profundidad Varias capas independientes; ninguna es la única JWT verificado en gateway y servicio; TLS y firma HMAC; validación y consultas parametrizadas
Fallar seguro Ante la duda, denegar; ante el error, no revelar AuthorizationPolicy con ALLOW explícito; 500 ERROR_INTERNO sin traza (04-02); config inválida → no arranca (04-03)
Superficie mínima Menos puertas, menos riesgo Inventario/Pagos/Notificaciones no expuestos (03-04); x-powered-by desactivado; imagen sin npm de desarrollo (05-01)
Seguridad por diseño Se decide al diseñar, no se parchea al final Propietario del recurso en el caso de uso; eventos sin datos innecesarios (02-05)
Shift left Comprobar lo antes posible en el ciclo Lint, pruebas, npm audit, Trivy y gitleaks en CI (05-03) antes de desplegar

  1. OWASP API Security Top 10 (2023) con ejemplos de TechCorp

El OWASP API Security Top 10 es la lista de referencia de riesgos en APIs. Recorrerla con casos propios es la mejor forma de interiorizarla:

# Riesgo Cómo se vería en TechCorp Remedio y dónde
API1 BOLA (Broken Object Level Authorization) Ana pide GET /v1/pedidos/ped-88214 (de otro cliente) y lo obtiene Comprobación de propietario en cada ruta con {id} (07-01): pedido ajeno → 404
API2 Autenticación rota Aceptar alg: none, tokens de horas, password grant, JWKS sin verificar aud jwtVerify con algorithms, issuer, audience; tokens de 5 min (07-01)
API3 Exposición excesiva de datos / propiedad a nivel de objeto GET /v1/pedidos/{id} devuelve el email y keycloakSub de la réplica clientes_ref; PATCH que acepta estado o total desde el cliente DTOs explícitos (aDto/aRepresentacion de 04-02/04-04): se eligen los campos que salen; esquemas zod con .strict() que rechazan campos no previstos
API4 Consumo de recursos sin límite Un cliente pide ?limite=100000, envía cuerpos de 50 MB o hace 10.000 POST /v1/pedidos por minuto limite máx. 100 (03-01/04-02), express.json({ limit: '100kb' }), rate limit 300/min en el gateway (03-04), timeout y bulkhead (06-03)
API5 Autorización rota a nivel de función Un cliente llama a GET /v1/pedidos?estado=PENDIENTE (listado de operador) o a POST /v1/productos requiereRol('operador') en rutas de operación (07-01); rutas de administración no expuestas por el gateway público
API6 Acceso sin restricción a flujos de negocio sensibles Un bot crea miles de pedidos de un producto en oferta y agota el stock Rate limit por usuario (no solo por IP), CAPTCHA en la web para picos, límites de negocio (máx. N unidades por pedido/cliente)
API7 SSRF Un webhook configurable por el usuario o una "URL de imagen" que Catálogo descarga apunta a http://10.0.0.5:15672 (RabbitMQ) o a http://169.254.169.254 (metadatos de la nube) No aceptar URLs arbitrarias del usuario; si es imprescindible, lista blanca de dominios, resolver DNS y rechazar IPs privadas, sin seguir redirecciones; egress restringido (07-04)
API8 Mala configuración CORS con *, x-powered-by, trazas de error al cliente, NODE_ENV distinto de production CORS con orígenes concretos (03-04), helmet (apartado 5), middlewareErrores que oculta detalles (04-02)
API9 Inventario de APIs deficiente Una ruta /v1/pedidos-legacy olvidada sin autenticación; una versión antigua aún desplegada OpenAPI como fuente de verdad (03-06), contratos Pact (04-05), el gateway como único punto de entrada, retirar versiones con fecha
API10 Consumo inseguro de APIs de terceros Confiar ciegamente en la respuesta de la pasarela de pago o de un proveedor de direcciones Validar también lo que llega de terceros con zod (el ACL traductorProducto de 04-04 hace eso), timeouts, HMAC en webhooks (07-02)

Los remedios ya existen en su mayoría en el código de TechCorp; esta lección los reúne y añade los que faltan.

  1. Validación y saneamiento de entrada con zod

Regla: todo lo que entra por la red se valida contra un esquema antes de tocarlo, tanto lo que viene del usuario como lo que viene de otro servicio o de un evento. En 04-02 y 04-04 ya se hace con zod; aquí se endurece:

// servicio-pedidos/src/rutas/esquemas.js
const { z } = require('zod');

// Identificadores con patrón cerrado: nada de "ped-88213' OR 1=1" ni de rutas con "../"
const PedidoId   = z.string().regex(/^ped-[0-9a-f]{8}$/, 'pedidoId no válido');
const ClienteId  = z.string().regex(/^c-[0-9]{1,10}$/);
const ProductoId = z.string().regex(/^p-[0-9]{1,10}$/);
const PAISES = ['ES', 'PT', 'FR'];                                         // lista blanca, no "cualquier ISO"

const Direccion = z.object({
  calle: z.string().trim().min(1).max(120),
  codigoPostal: z.string().regex(/^[0-9]{5}$/),
  ciudad: z.string().trim().min(1).max(80),
  pais: z.enum(PAISES)
}).strict();                                                               // campos desconocidos → 400 (no se ignoran en silencio)

const CrearPedido = z.object({
  clienteId: ClienteId,
  lineas: z.array(z.object({ productoId: ProductoId, cantidad: z.number().int().min(1).max(50) }).strict()).min(1).max(30),
  direccionEnvio: Direccion
}).strict();

const ListarPedidos = z.object({
  estado: z.enum(['PENDIENTE', 'CONFIRMADO', 'CANCELADO']).optional(),
  clienteId: ClienteId.optional(),
  limite: z.coerce.number().int().min(1).max(100).default(20),
  cursor: z.string().max(200).optional()
}).strict();

module.exports = { PedidoId, CrearPedido, ListarPedidos };

Y en las rutas, también los parámetros de ruta, que muchos olvidan:

// servicio-pedidos/src/rutas/pedidos.js (fragmentos)
enrutador.get('/v1/pedidos/:id', requiereScope('pedidos:leer'), async (req, res, next) => {
  try {
    const id = PedidoId.parse(req.params.id);          // ZodError → 400 PETICION_INVALIDA por middlewareErrores (04-02)
    // ... propietario (07-01), repositorio.obtener(id), ETag ...
  } catch (err) { next(err); }
});
enrutador.post('/v1/pedidos', requiereScope('pedidos:crear'), async (req, res, next) => {
  try {
    const peticion = CrearPedido.parse(req.body);      // a partir de aquí, "peticion" es de confianza en forma; el negocio decide el resto
    // ...
  } catch (err) { next(err); }
});

Y en app.js los límites globales que ya traía la plantilla, ahora justificados: express.json({ limit: '100kb', strict: true }) (un cuerpo mayor → 413; strict rechaza JSON que no sea objeto o array), y en el gateway proxy-body-size: 2m en el Ingress (05-02) como techo absoluto. Saneamiento frente a validación: TechCorp prefiere rechazar (400) a "limpiar" silenciosamente; la única normalización aceptada es trim() y la de mayúsculas en códigos (pais.toUpperCase()), y siempre en el esquema, nunca dispersa.

  1. Inyección: SQL, NoSQL, comandos y logs

La inyección ocurre cuando datos del usuario se interpretan como código o estructura. Cuatro variantes que afectan a TechCorp:

SQL (PostgreSQL con pg). Nunca concatenar; siempre parámetros posicionales, como ya hace PedidoRepositorio en 04-04:

// MAL: si estado = "PENDIENTE' OR '1'='1", devuelve todo; si contiene "; DROP TABLE pedidos; --", peor
await bd.query(`SELECT * FROM pedidos.pedidos WHERE estado = '${estado}'`);
// BIEN: el valor viaja aparte de la sentencia; el motor nunca lo interpreta como SQL
await bd.query('SELECT * FROM pedidos.pedidos WHERE estado = $1 AND cliente_id = $2 ORDER BY creado_en DESC LIMIT $3', [estado, clienteId, limite]);
// Los identificadores (nombre de columna para ORDER BY) NO se pueden parametrizar: lista blanca
const COLUMNAS_ORDEN = { creadoEn: 'creado_en', total: 'total_centimos' };
const columna = COLUMNAS_ORDEN[orden] ?? 'creado_en';

NoSQL (MongoDB en Catálogo). El riesgo es distinto: no es texto, sino objetos. Si una ruta hace coleccion.find({ sku: req.query.sku }) y el cliente envía ?sku[$ne]=x, Express (con qs) construye { sku: { $ne: 'x' } } y la consulta devuelve todos los productos; con $where o $regex puede ser peor. Regla: nunca pasar objetos del usuario al filtro; validar con zod que cada valor es una cadena (o número) y construir el filtro en el repositorio con las claves fijas:

// servicio-catalogo/src/rutas/productos.js
const Consulta = z.object({ ids: z.string().optional(), categoria: z.string().regex(/^[a-z-]{1,40}$/).optional(), limite: z.coerce.number().int().min(1).max(100).default(20) }).strict();
const q = Consulta.parse(req.query);                   // ?categoria[$ne]=x → falla: no es string
// repositorio: const filtro = {}; if (q.categoria) filtro.categoria = q.categoria;  // la clave la pone el código, el valor es un string validado

Además, express.json no interpreta operadores, pero un cuerpo { "filtro": { "$where": "..." } } sí llegaría como objeto: mismo remedio (esquema .strict(), nada de filtro libre).

Comandos del sistema. Casi nunca hacen falta en un servicio; si los hay (generar un PDF, convertir una imagen), execFile('convert', [ruta]) con argumentos en array, jamás exec(\convert ${nombre}`)`, y el nombre de fichero validado con patrón. Mejor aún: una librería nativa.

Logs. El log injection: un usuario pone en calle el texto "Gran Vía 12\n{\"nivel\":\"info\",\"mensaje\":\"pedido pagado\"}" y, si el log fuera texto, aparecería una línea falsa. Con pino (06-01) el valor va serializado dentro de un JSON (los saltos de línea se escapan como \n), así que la estructura no se rompe; la regla es no construir mensajes concatenando entrada del usuario (log.info(\pedido de ${nombre}`)) sino pasarla como campo (log.info({ clienteId }, 'pedido creado')`), y recordar que los datos personales no van al log en ningún caso (apartado 8).

  1. Cabeceras de seguridad con helmet

helmet es un conjunto de middlewares de Express que fija cabeceras HTTP defensivas. En el gateway (lo que ve el navegador) y en cada servicio (defensa en profundidad; y algunos, como el BFF, sirven HTML):

// gateway/servidor.js y plantilla de servicio (app.js), tras middlewareRequestId
const helmet = require('helmet');
app.use(helmet({
  contentSecurityPolicy: false,          // la API no sirve HTML; la CSP la define la SPA/BFF que sí lo sirve
  strictTransportSecurity: { maxAge: 31_536_000, includeSubDomains: true },   // HSTS (07-02): coherente con el Ingress
  crossOriginResourcePolicy: { policy: 'same-site' }
}));
Cabecera Efecto Quién la necesita
Strict-Transport-Security Solo HTTPS durante un año Gateway (y el Ingress, 07-02)
X-Content-Type-Options: nosniff El navegador no "adivina" tipos MIME (una respuesta JSON no se ejecuta como script) Todos
X-Frame-Options: DENY / CSP frame-ancestors Evita clickjacking BFF/web; inocua en APIs
Content-Security-Policy Qué orígenes de script/estilo se permiten tienda-web y bff-movil si sirven HTML; desactivada en la API
Referrer-Policy, X-DNS-Prefetch-Control, Cross-Origin-* Fugas de información al navegar Web
(elimina) X-Powered-By No anunciar Express Todos (app.disable('x-powered-by') ya en 04-02)

helmet no sustituye a CORS (03-04): CORS dice desde qué origen puede llamar el navegador; helmet dice cómo debe tratar el navegador la respuesta.

  1. Gestión de dependencias

Un servicio-pedidos típico arrastra 300 paquetes transitivos; la mayoría de las vulnerabilidades reales entran por ahí. La política de TechCorp:

Práctica Cómo Cuándo
package-lock.json versionado y npm ci El lockfile fija versiones exactas; npm ci falla si no coincide con package.json (05-01, 05-03) Siempre
npm audit --omit=dev --audit-level=high Falla el pipeline si hay vulnerabilidad alta/crítica con parche en dependencias de producción En ci.yml, etapa de lint
Dependabot o Renovate Pull requests automáticos de actualización, agrupados por semana; los parches de seguridad, inmediatos Configuración en .github/dependabot.yml
Política de actualización Menores y parches: se aceptan si pasan CI y contrato; mayores: revisión del equipo Regla de Luis (05-03)
Dependencias mínimas Antes de añadir un paquete: ¿lo trae Node? (fetch, crypto, AbortSignal), ¿está mantenido?, ¿cuántos transitivos arrastra? Revisión de PR
Fijar la imagen base y las acciones de CI node:20-alpine con digest (07-04), actions/checkout@v4 por versión Plantilla
SBOM Lista de materiales de cada imagen (Syft, 07-04) para saber en minutos si "usamos la librería X" Mención; se genera en CI

Y npm audit en CI no es infalible: revisa el aviso, no lo silencies con --force; si no hay parche, evalúa el riesgo (¿la función vulnerable se usa?) y documenta la excepción con fecha de revisión.

  1. Secretos en el código: nunca, y cómo asegurarlo

La regla existe desde 04-03 (los secretos llegan por el entorno o ficheros _FILE, nunca en el repositorio ni en la imagen); aquí se hace verificable:

# .github/workflows/servicio-node-ci.yml (fragmento, el workflow reutilizable de 05-03)
      - name: Buscar secretos en el historial
        uses: gitleaks/gitleaks-action@v2          # detecta claves AWS, tokens, contraseñas en URLs, claves privadas...
        env: { GITLEAKS_LICENSE: "" }               # gratuito para repos de organización con licencia; alternativa: git-secrets o trufflehog

Y en local, un hook de pre-commit con gitleaks protect --staged. Si un secreto llega al repositorio, la única respuesta correcta es rotarlo (el historial de git es público para siempre para quien lo haya clonado): la mecánica de rotación —Secrets, External Secrets, reinicios— es de 07-04. Añade a .gitignore de la plantilla .env, *.pem, *.key; y recuerda que un secreto en un console.log, en una traza de error o en la URL de una petición (?token=) también es una fuga: por eso redact (06-01) y configParaLog() (04-03).

  1. Protección de datos personales: RGPD en la práctica

TechCorp trata datos personales de Ana (nombre, correo, dirección) y datos de pago. Sin pretender sustituir al responsable de protección de datos, las decisiones técnicas que un equipo debe tomar:

  • Minimización. ¿Debe pedido.creado llevar cliente.email y nombre (02-02)? Se necesitan para que Notificaciones envíe el correo sin llamar a Clientes (autonomía, 03-02). Alternativas: (a) el evento lleva los datos y todos los consumidores los reciben (Inventario no los necesita); (b) el evento lleva solo clienteId y Notificaciones consulta GET /v1/clientes/{id} con su token de servicio. Decisión de TechCorp: (a) por autonomía y porque Notificaciones es el único suscriptor que los usa hoy, con tres condiciones: los datos personales solo van en pedido.creado (no en el resto de eventos de la saga), RabbitMQ va cifrado (07-02) y las colas y la DLQ tienen retención limitada (mensajes en .dlq más de 7 días se purgan tras revisarse: no son un archivo); y se revisará hacia (b) si aparece un segundo consumidor que no los necesita.
  • Seudonimización. Fuera de Clientes, los servicios guardan clienteId, no el nombre; los logs y métricas llevan solo identificadores (06-01); las trazas de Jaeger no llevan cuerpos.
  • Cifrado en reposo. En las bases de datos gestionadas está activo por defecto (discos cifrados); para columnas especialmente sensibles se puede cifrar a nivel de aplicación con una clave del gestor de secretos, a costa de no poder indexarlas. Copias de seguridad cifradas y con la misma retención.
  • Derecho de supresión. El flujo de 02-04: servicio-clientes anonimiza y publica cliente.eliminado; Pedidos borra clientes_ref y anonimiza direcciones de pedidos antiguos que la normativa fiscal obligue a conservar; Notificaciones no guarda nada. Es un caso de uso más, con prueba automatizada.
  • Datos de tarjeta: NUNCA en TechCorp. El navegador envía la tarjeta directamente a la pasarela (formulario o SDK del proveedor); TechCorp recibe un token (tok_…) que servicio-pagos usa para cobrar. Así el alcance de PCI DSS se reduce al mínimo (SAQ A). Ni el número, ni el CVV, ni siquiera "los cuatro últimos dígitos" salvo que la pasarela los devuelva ya enmascarados. *.tarjeta en redact es una red por si alguien se equivoca, no un permiso.
  • Registro de tratamientos y retención: cada servicio documenta qué datos personales guarda, para qué y cuánto tiempo (tabla en su README), y un CronJob aplica la retención (p. ej. anonimizar direcciones de pedidos de más de 5 años).

  1. Auditoría de acciones sensibles

Los logs de 06-01 cuentan qué pasó técnicamente; la auditoría cuenta quién hizo qué sobre qué, y debe poder demostrarse meses después. Acciones auditables en TechCorp: cancelar un pedido (quién, motivo), cambiar un precio en Catálogo, cambiar el estado de un pago manualmente, reprocesar la DLQ (scripts/reprocesarDlq.js, 06-03), acceder a datos de un cliente por parte de un operador, cambiar roles en Keycloak. Requisitos: inmutable (solo inserción, sin UPDATE/DELETE para el usuario svc_*), separado de los logs operativos (retención distinta: años frente a semanas), y correlacionable con X-Request-Id y el sub del token (X-Usuario-Id, 07-01).

// @techcorp/comun-http/src/auditoria.js — un registro por acción sensible
function crearAuditoria({ bd, servicio }) {
  return {
    async registrar(req, { accion, recurso, recursoId, detalle = {} }) {
      // tabla <esquema>.auditoria(id, ocurrido_en, servicio, accion, recurso, recurso_id, usuario_id, roles, request_id, detalle jsonb)
      // El usuario svc_* solo tiene INSERT sobre ella; la lectura, un rol aparte (auditor)
      await bd.query(
        'INSERT INTO auditoria (ocurrido_en, servicio, accion, recurso, recurso_id, usuario_id, roles, request_id, detalle) VALUES (now(), $1, $2, $3, $4, $5, $6, $7, $8)',
        [servicio, accion, recurso, recursoId, req.usuario?.sub ?? 'sistema', (req.usuario?.roles ?? []).join(','), req.id, JSON.stringify(detalle)]
      );
    }
  };
}
// Uso en la cancelación (03-01): await auditoria.registrar(req, { accion: 'PEDIDO_CANCELADO', recurso: 'pedido', recursoId: pedido.pedidoId, detalle: { motivo } });

Con volumen alto, la tabla se sustituye por eventos auditoria.* a un almacén de solo escritura (o el log estructurado con tipo: 'auditoria' enviado a un tenant de Loki con retención larga y sin borrado); lo importante es la separación y la inmutabilidad, no la tecnología. detalle no contiene datos personales: identificadores y valores de negocio.

  1. Pruebas de seguridad: SAST, DAST, revisión y pentest

Tipo Qué hace Herramienta en TechCorp Cuándo
SAST (análisis estático) Busca patrones peligrosos en el código sin ejecutarlo eslint-plugin-security en la configuración de la plantilla; Semgrep con las reglas p/nodejs y p/owasp-top-ten en CI Cada PR
Análisis de dependencias Vulnerabilidades conocidas npm audit, Dependabot (apartado 6) Cada PR y diario
Escaneo de imágenes CVEs en la imagen Trivy (07-04) Cada build
DAST (dinámico) Ataca la API desplegada OWASP ZAP en modo API (zap-api-scan.py con el OpenAPI de 03-06) contra staging, tras la E2E de 05-03 Cada despliegue a staging (informe), semanal (bloqueante en alto)
Revisión de código Ojos humanos con lista de comprobación Lista del apartado 12 en la plantilla de PR; revisor de otro equipo para rutas con {id} o datos personales Cada PR
Pentest Un profesional externo intenta romperlo Empresa externa; alcance: gateway, tienda-web, Keycloak, webhooks Anual y antes de la auditoría de Pagos
# .github/workflows/servicio-node-ci.yml (fragmento SAST)
      - name: Semgrep
        uses: semgrep/semgrep-action@v1
        with: { config: "p/nodejs p/owasp-top-ten p/secrets" }

Y las pruebas de seguridad de aplicación son pruebas normales: el test/pedidos.auth.test.js de 07-01 (401/403/404) y casos como "un id que no cumple el patrón devuelve 400", "un cuerpo con campo desconocido devuelve 400", "?categoria[$ne]=x devuelve 400" viven en Jest junto al resto y protegen contra regresiones.

  1. Respuesta ante vulnerabilidades

Antes o después alguien encontrará algo. Lo que TechCorp deja preparado:

  1. Canal de reporte: [email protected] y un fichero /.well-known/security.txt en shop.techcorp.example (RFC 9116) con contacto y política; agradecimiento público (sin recompensa formal en la fase 1).
  2. Triaje: gravedad con CVSS; crítica/alta → incidente de seguridad con el proceso de 06-05 (canal, responsable, cronología), aunque no haya caída de servicio.
  3. Parcheo: rama desde main, prueba que reproduce el fallo, despliegue por el pipeline normal (canary si aplica, 05-04); dependencia vulnerable → actualización o mitigación temporal (bloquear la ruta en el gateway).
  4. Comunicación: interna (todos los equipos, porque el mismo patrón puede estar en otro servicio) y, si ha habido acceso a datos personales, notificación a la autoridad de control en 72 horas y a los afectados según el RGPD: decisión del responsable de protección de datos, no del equipo técnico.
  5. Postmortem sin culpa (06-05) con acciones: nueva regla de Semgrep, nuevo caso en la lista de comprobación, prueba de regresión.

  1. Lista de comprobación de seguridad por servicio

Esta tabla se añade a techcorp/plantilla-servicio-node/SEGURIDAD.md y a la plantilla de pull request; cada servicio la revisa al crearse y en cada cambio relevante:

Área Comprobación Referencia
Autenticación autenticar() en /v1/*; sondas y /metrics sin token; algorithms, issuer, audience fijados 07-01
Autorización Toda ruta con {id} comprueba propietario o rol; requiereScope/requiereRol en rutas de operación 07-01, API1/API5
Entrada Esquemas zod .strict() para cuerpo, query y parámetros de ruta; ids con patrón; limite ≤ 100; express.json({ limit }) Apartado 3, API4
Salida DTOs explícitos; nunca email/keycloakSub de terceros; errores sin traza Apartado 2 (API3), 04-02
Inyección Consultas parametrizadas; filtros MongoDB con claves fijas; sin exec con entrada del usuario; log por campos Apartado 4
Cabeceras helmet, x-powered-by off, CORS con orígenes concretos Apartado 5
Dependencias npm ci, npm audit en CI, Dependabot activo, sin paquetes sin mantener Apartado 6
Secretos Ninguno en repo/imagen/log; gitleaks en CI; redact y configParaLog() cubren los nuevos Apartado 7, 07-04
Datos personales Tabla de tratamientos en el README; retención con CronJob; reacciona a cliente.eliminado si guarda datos de clientes; nada de tarjetas Apartado 8
Auditoría Acciones sensibles registradas con usuario_id y request_id; tabla de solo inserción Apartado 9
Pruebas Casos 400/401/403/404 en Jest; Semgrep y ZAP en verde o con excepciones documentadas Apartado 10
Canal TLS a BD y broker, usuario svc_* mínimo 07-02
Plataforma securityContext, NetworkPolicy, ServiceAccount propio 07-04

Errores Comunes y Consejos

  • Validar solo el cuerpo y dejar req.params.id y req.query sin esquema: la mitad de las inyecciones y BOLA entran por ahí.
  • Esquemas permisivos (z.object sin .strict(), z.string() sin max) que aceptan cualquier campo o cadenas de 10 MB: la validación debe ser tan estricta como el contrato OpenAPI.
  • "Sanear" en vez de rechazar: quitar caracteres "peligrosos" con expresiones regulares caseras nunca cubre todos los casos y cambia los datos del usuario a escondidas.
  • Confiar en la respuesta de terceros porque "es la pasarela": API10 existe porque los terceros también se equivocan y también los atacan.
  • Loguear el cuerpo de la petición en info "para depurar" y descubrir semanas después que Loki guarda direcciones y correos.
  • Silenciar npm audit con --force o audit-level=critical para que CI pase: cada excepción, documentada con fecha.
  • Considerar la auditoría "un log más" y borrarla a los 30 días con la retención de Loki.
  • Consejo: cada regla de esta lección que se pueda automatizar (lint, Semgrep, gitleaks, ZAP, prueba Jest) vale más que la misma regla en un documento; la lista de comprobación es para lo que no se puede automatizar.

Ejercicios

Ejercicio 1. Un desarrollador propone añadir a servicio-catalogo la ruta GET /v1/productos/buscar?filtro=<json> que recibe un objeto de filtro de MongoDB "para que el front pueda hacer consultas flexibles". Explica el problema con la numeración de OWASP y propón un diseño seguro que cubra las necesidades reales (buscar por texto, categoría y rango de precio).

Ejercicio 2. Escribe el esquema zod para POST /v1/pedidos/{id}/cancelacion (03-01: motivo de una lista permitida, comentario opcional) incluyendo el parámetro de ruta, y el registro de auditoría correspondiente con crearAuditoria.

Ejercicio 3. Marta pregunta si, para cumplir el RGPD, basta con que Notificaciones "borre el correo cuando llegue cliente.eliminado". Enumera qué otros lugares de la plataforma pueden contener el correo de Ana y qué medida aplica a cada uno.

Soluciones

Solución 1. Es API8/API3 y una inyección NoSQL de manual: un objeto de filtro arbitrario permite { "$where": "..." }, { "precio": { "$gt": 0 } } sobre campos que no debían filtrarse, exponer campos internos (costeProveedor), consultas sin índice que tumban Mongo (API4) y, con $lookup en agregaciones, leer otras colecciones. Diseño seguro: parámetros con nombre y esquema cerrado, Busqueda = z.object({ q: z.string().trim().min(2).max(60).optional(), categoria: z.string().regex(/^[a-z-]{1,40}$/).optional(), precioMin: z.coerce.number().min(0).optional(), precioMax: z.coerce.number().max(100000).optional(), limite: ...máx 100, cursor }).strict(); el repositorio construye el filtro con claves fijas ({ nombreNormalizado: { $regex: escapar(q), $options: 'i' } } con q escapado o, mejor, un índice de texto y $text: { $search: q }; precioCentimos: { $gte, $lte }); solo campos indexados; y la ruta devuelve el DTO aDto de siempre. Si el front necesita algo más flexible, es un caso para GraphQL en el BFF (03-03), con esquema tipado, no para pasar objetos de Mongo.

Solución 2.

const MOTIVOS = ['CLIENTE_ARREPENTIDO', 'ERROR_PEDIDO', 'SIN_STOCK', 'FRAUDE'];
const Cancelacion = z.object({ motivo: z.enum(MOTIVOS), comentario: z.string().trim().max(300).optional() }).strict();

enrutador.post('/v1/pedidos/:id/cancelacion', requiereScope('pedidos:crear'), async (req, res, next) => {
  try {
    const id = PedidoId.parse(req.params.id);
    const { motivo, comentario } = Cancelacion.parse(req.body);
    const pedido = await repositorio.obtener(id);
    // propietario/rol como en 07-01 (cliente: solo el suyo → si no, 404; operador: cualquiera; FRAUDE solo operador)
    if (motivo === 'FRAUDE' && !req.usuario.roles.includes('operador')) throw new ErrorNegocio('PROHIBIDO', 'Motivo reservado a operadores', 403);
    await cancelarPedido({ pedido, motivo, usuario: req.usuario });   // transición + outbox pedido.cancelado (04-04)
    await auditoria.registrar(req, { accion: 'PEDIDO_CANCELADO', recurso: 'pedido', recursoId: id, detalle: { motivo, tieneComentario: Boolean(comentario) } });
    res.status(202).set('Location', `/v1/pedidos/${id}`).end();
  } catch (err) { next(err); }
});

El comentario no va a la auditoría (texto libre del usuario, puede contener datos personales): solo si existe. Y la auditoría se registra después de la transición, dentro de la misma transacción si el repositorio lo permite, para no auditar cancelaciones que fallaron.

Solución 3. Lugares y medidas: (1) Base de datos de Clientes: anonimizar la fila (no borrar, por integridad y obligaciones fiscales), origen del evento. (2) clientes_ref en Pedidos (02-04): borrar la fila y anonimizar direccion_envio de pedidos antiguos al recibir cliente.eliminado. (3) Mensajes pedido.creado en colas, .reintento y .dlq de RabbitMQ: retención corta y purga (apartado 8); si hay mensajes en DLQ del cliente, se procesan o descartan. (4) Logs en Loki: no debería haber correo gracias a redact y a la regla de no loguearlo; si una auditoría encuentra alguno, es un incidente (apartado 11) y hay que borrarlo del almacén. (5) Trazas en Jaeger: sin cuerpos ni datos personales por diseño (06-02); comprobar los atributos de span propios. (6) Copias de seguridad: no se editan; se documenta que el dato desaparece al expirar la retención de la copia (y se anonimiza al restaurar). (7) Keycloak: borrar o desactivar el usuario (el sub), que es donde vive el correo de login. (8) Proveedor de correo de Notificaciones (si conserva historial de envíos): configurar retención mínima o solicitar borrado por su API. (9) La tabla de auditoría: no debe contener correos (solo usuario_id), y por diseño se conserva. La respuesta a Marta: "borrar en Notificaciones" es una de nueve acciones, y por eso el derecho de supresión es un caso de uso con lista y prueba, no un DELETE.

Conclusión

Esta lección ha convertido la seguridad "del código" en prácticas concretas de TechCorp: seis principios (mínimo privilegio, defensa en profundidad, fallar seguro, superficie mínima, seguridad por diseño, shift left); el OWASP API Security Top 10 leído con casos propios y remedios que ya existían (propietario del recurso, DTOs, limite ≤ 100, rate limit, CORS, OpenAPI y Pact) o se han añadido; esquemas zod .strict() con ids por patrón (^ped-[0-9a-f]{8}$), listas blancas y límites, también en parámetros de ruta y query, con express.json({ limit: '100kb' }); consultas parametrizadas en pg, filtros de MongoDB con claves fijas, execFile y logs por campos; helmet en gateway y servicios; npm ci, npm audit, Dependabot y SBOM; gitleaks en CI y rotación ante cualquier fuga; RGPD práctico (minimización con la decisión de mantener email en pedido.creado bajo condiciones, seudonimización, cifrado en reposo, cliente.eliminado, retención, y nunca datos de tarjeta: tokenización de la pasarela y alcance PCI DSS mínimo); auditoría inmutable y separada con crearAuditoria correlacionada por usuario_id y request_id; SAST con ESLint/Semgrep, DAST con ZAP contra staging, revisión con lista y pentest anual; un proceso de respuesta con security.txt y notificación RGPD; y la lista de comprobación que ahora forma parte de plantilla-servicio-node. Queda la última capa, la que sostiene a todas las demás: la imagen que ejecuta el código, el pod que la contiene, los secretos que le llegan, la red que lo rodea y los permisos del clúster. Es el tema de la siguiente lección: seguridad en contenedores y Kubernetes.

Curso de Microservicios

Módulo 1: Introducción a los Microservicios

Módulo 2: Diseño de Microservicios

Módulo 3: Comunicación entre Microservicios

Módulo 4: Implementación de Microservicios

Módulo 5: Despliegue y Orquestación

Módulo 6: Monitoreo y Mantenimiento

Módulo 7: Seguridad en Microservicios

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

© Copyright 2026. Todos los derechos reservados