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
- Principios que ordenan el resto
- OWASP API Security Top 10 (2023) con ejemplos de TechCorp
- Validación y saneamiento de entrada con
zod - Inyección: SQL, NoSQL, comandos y logs
- Cabeceras de seguridad con
helmet - Gestión de dependencias
- Secretos en el código: nunca, y cómo asegurarlo
- Protección de datos personales: RGPD en la práctica
- Auditoría de acciones sensibles
- Pruebas de seguridad: SAST, DAST, revisión y pentest
- Respuesta ante vulnerabilidades
- Lista de comprobación de seguridad por servicio
- 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 |
- 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.
- Validación y saneamiento de entrada con
zod
zodRegla: 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.
- 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 validadoAdemá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).
- Cabeceras de seguridad con
helmet
helmethelmet 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.
- 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.
- 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 trufflehogY 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).
- 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.creadollevarcliente.emailynombre(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 soloclienteIdy Notificaciones consultaGET /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 enpedido.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.dlqmá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-clientesanonimiza y publicacliente.eliminado; Pedidos borraclientes_refy 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_…) queservicio-pagosusa 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.*.tarjetaenredactes 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 unCronJobaplica la retención (p. ej. anonimizar direcciones de pedidos de más de 5 años).
- 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.
- 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.
- Respuesta ante vulnerabilidades
Antes o después alguien encontrará algo. Lo que TechCorp deja preparado:
- Canal de reporte:
[email protected]y un fichero/.well-known/security.txtenshop.techcorp.example(RFC 9116) con contacto y política; agradecimiento público (sin recompensa formal en la fase 1). - 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.
- 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). - 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.
- Postmortem sin culpa (06-05) con acciones: nueva regla de Semgrep, nuevo caso en la lista de comprobación, prueba de regresión.
- 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.idyreq.querysin esquema: la mitad de las inyecciones y BOLA entran por ahí. - Esquemas permisivos (
z.objectsin.strict(),z.string()sinmax) 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 auditcon--forceoaudit-level=criticalpara 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
- Conceptos Básicos de Microservicios
- Ventajas y Desventajas de los Microservicios
- Comparación con la Arquitectura Monolítica
- Cuándo Adoptar Microservicios: Criterios de Decisión
- El Caso Práctico del Curso: la Tienda Online de TechCorp
Módulo 2: Diseño de Microservicios
- Principios de Diseño de Microservicios
- Descomposición de Aplicaciones Monolíticas
- Definición de Bounded Contexts
- Gestión de Datos: una Base de Datos por Servicio
- Consistencia Distribuida: Sagas, CQRS y Event Sourcing
Módulo 3: Comunicación entre Microservicios
- APIs RESTful
- Mensajería Asíncrona
- Protocolos de Comunicación: gRPC, GraphQL
- API Gateway y Backend for Frontend
- Descubrimiento de Servicios y Balanceo de Carga
- Contratos y Versionado de APIs
Módulo 4: Implementación de Microservicios
- Elección de Tecnologías y Herramientas
- Desarrollo de un Microservicio Simple
- Gestión de Configuración
- Integración Práctica: Consumir APIs y Publicar Eventos
- Pruebas en Microservicios: Unitarias, de Integración y de Contrato
Módulo 5: Despliegue y Orquestación
- Contenedores y Docker
- Orquestación con Kubernetes
- CI/CD para Microservicios
- Estrategias de Despliegue: Rolling, Blue-Green y Canary
- Service Mesh: Istio y Linkerd
Módulo 6: Monitoreo y Mantenimiento
- Monitoreo y Logging
- Trazabilidad Distribuida con OpenTelemetry
- Gestión de Errores y Recuperación
- Escalabilidad y Rendimiento
- SLOs, Alertas y Gestión de Incidentes
Módulo 7: Seguridad en Microservicios
- Autenticación y Autorización
- Seguridad en la Comunicación
- Prácticas de Seguridad
- Seguridad en Contenedores y Kubernetes
