35 26. Necesitamos defendernos de lo que el frontend no puede ver
El checklist de seguridad antes de exponer una API
Todo lo anterior fue construir seguridad pieza por pieza. Este capítulo es auditar: recorrer tus endpoints reales con el OWASP API Top 10 en la mano y responder “¿dónde rompería esto si fuera el atacante?”. La mayoría de las defensas ya las tienes — este capítulo es demostrarlo y tapar lo que falta.
35.1 El problema
La API funciona, los tests pasan… y alguien pega en el chat un ?user_id=1 que devuelve datos de otro usuario. La seguridad no es una feature que se agrega: es una propiedad que se audita — cada endpoint, cada input, cada error. OWASP no es teoría de conferencia: es la lista de las formas reales en que APIs como la tuya se rompen cada semana.
Un array es una colección ordenada de elementos accedidos por posición: ["a","b","c"][0] es "a" (se cuenta desde 0). Python las llama listas, Go slices — misma idea, distinto acento.
return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.
35.2 Cómo lo resuelve un equipo
Un equipo serio hace la auditoría sobre los endpoints que existen, no sobre un checklist abstracto: por cada ruta — ¿quién puede llamarla, qué devuelve, qué pasa con input malicioso, qué loguea? La OWASP API Top 10 traducida a Taskflow es la tabla de abajo: cada amenaza mapea a un control que ya construimos en los capítulos 4–18 — la auditoría es verificar que cada control está activo en cada endpoint, no en los que te acuerdas.
35.3 Conceptos nuevos
- U OWASP API Top 10 traducido a tus endpoints: BOLA, auth rota, exposición de datos
- U Inyección (SQL, comandos): por qué parámetros/ORM es tu defensa y dónde aún puedes fallar
- U Headers de seguridad en una API pura: HSTS, X-Content-Type-Options
- U Rate limiting generalizado: qué endpoints se protegen primero
- U Validación como seguridad: el 422 también es una muralla
35.4 La auditoría — OWASP API Top 10 contra Taskflow
| Amenaza OWASP | En Taskflow significa | Tu control (capítulo) | Estado |
|---|---|---|---|
| BOLA — Broken Object Level Authorization | GET /tasks/{id} devuelve tareas ajenas |
WHERE user_id en la query + 404 a lo ajeno (cap. 17) |
✅ por endpoint — auditar cada uno |
| Broken Authentication | login sin límite, tokens eternos | bcrypt, JWT corto, rate limit, refresh rotado (cap. 14–18) | ✅ |
| Excessive Data Exposure | GET /me filtra password_hash |
Contratos de salida whitelist (cap. 5, 16) | ✅ |
| Unrestricted Resource Consumption | body gigante, uploads infinitos, listas sin paginar | Límites de body/upload, paginación, rate limit (cap. 12, 18, 22) | ✅ parcial |
| Broken Function Level Authorization | viewer llamando DELETE /projects |
requireRole por endpoint (cap. 17) |
⚠️ auditar uno por uno |
| SSRF | “dame la URL del avatar a descargar” → tu servidor visita 169.254.169.254 |
Allowlist de hosts si algún día aceptas URLs | ⚠️ diseño |
| Security Misconfiguration | headers ausentes, debug on, errores verbosos | Handler global + headers (abajo) | ⚠️ este capítulo |
| Injection | WHERE email = '" + email + "' |
Queries parametrizadas siempre (cap. 9) | ✅ por disciplina |
| Improper Inventory | /v1/tasks viejo sin auth aún vivo |
Versionar y matar lo viejo — inventario de rutas | ⚠️ proceso |
| Unsafe Consumption of APIs | Creer lo que el webhook dice sin verificar | Firma HMAC + dedupe (cap. 22) | ✅ |
Los ✅ no son permanentes: se ganan por endpoint y por PR — por eso la auditoría es una tabla viva, no un recuerdo.
35.5 Implementación
Capítulo agnóstico — dos piezas que faltaban:
Headers de seguridad (un middleware, cualquier stack):
Strict-Transport-Security: max-age=31536000 # HTTPS siempre
X-Content-Type-Options: nosniff # no adivines el tipo
X-Frame-Options: DENY # tu API no se embebe
Cache-Control: no-store # respuestas de API no se cachean
El checklist por endpoint (la auditoría operativa):
GET /tasks/{id}
[ ] auth requerida (middleware)
[ ] ownership en la query (WHERE user_id)
[ ] 404 para lo ajeno, no 403 (anti-enumeración)
[ ] solo campos del contrato público
[ ] query parametrizada
[ ] rate limit si es sensible
[ ] errores por el handler global (sin stack trace)
Audita la seguridad de mi API [Express/FastAPI/net-http] contra OWASP
API Top 10. Para cada endpoint: identifica la amenaza aplicable (BOLA,
function-level auth, data exposure, injection, resource consumption),
verifica el control que YA existe en el código (ownership en query,
requireRole, contrato de salida, parametrización, rate limit, headers),
y genera la tabla endpoint × amenaza × control × estado con las
correcciones concretas de lo que falte. Incluye tests de regresión para
cada control (viewer→403, ajeno→404, injection→literal).
35.6 ¿Por qué así y no “instalar un plugin de seguridad”?
Lo único que necesitas llevarte: la seguridad de API no es una librería que se enchufa — son ~8 decisiones ya tomadas que deben estar activas en cada endpoint. El plugin pone headers; no pone WHERE user_id en tu query.
Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.
| Te lo da el framework/librería | Sigue siendo tuyo |
|---|---|
| Headers de seguridad (helmet, secure-headers) | BOLA: WHERE user_id en cada query |
| Parseo seguro de JSON/multipart | Contratos de salida (whitelist) |
| TLS terminado por el proxy | Rate limiting por endpoint sensible |
| Query builders parametrizados | Roles por recurso, dedupe de webhooks |
La regla honesta: las herramientas cubren el transporte; la seguridad de tu dominio (quién toca qué) la escribes tú o no existe.
35.7 Errores comunes
| Error | Por qué pasa | Fix |
|---|---|---|
| “Ya está securizado” por memoria | El control existe en 7 de 9 endpoints | La tabla endpoint×control — auditar, no recordar |
| Confiar la seguridad al frontend | “El botón está oculto para viewers” | El navegador miente; la API enforza — siempre |
| CORS como defensa | “Solo mi dominio puede llamar” | CORS protege al usuario, curl no pide permiso (cap. 18) |
| Errores verbosos en prod | Debug útil | Stack trace = mapa del código para el atacante → handler global |
| Rate limit solo en login | “Ahí era el problema” | También refresh, register, forgot-password, OTP, endpoints caros |
| Endpoints viejos vivos | /v1/tasks quedó sin auth tras la migración |
Inventario de rutas: lo que no se audita, se expone |
35.8 Buenas prácticas
- La tabla de auditoría vive en el repo (docs/SECURITY.md): cada endpoint nuevo agrega su fila en el mismo PR — seguridad como checklist mergeable, no como auditoría anual.
- Tests de regresión de seguridad (cap. 28):
viewer → 403,ajeno → 404,inyección → literalson tests permanentes — el control que no se testea se rompe sin ruido. - Principio de privilegio mínimo en todo: el usuario de BD de la app no es
postgres; el token no vive 30 días; el endpoint no devuelve campos “por si acaso”.
Un token es una credencial portable: una cadena que dice quién eres y hasta cuándo. El servidor la emite tras el login; el cliente la presenta en cada request en vez de la contraseña.
- Asumir que el cliente es hostil: toda entrada se valida (cap. 4) — el 422 es seguridad, no solo UX.
35.9 Ejercicio
- Un pentester reporta: “
GET /tasks/41da 403 yGET /tasks/42da 404 — así sé que la 41 existe y es de otro”. ¿Qué está mal y cómo se arregla? - Tu API acepta
GET /avatar?url=...y el servidor descarga esa URL. ¿Qué amenaza OWASP es y cuál es la mitigación? - De la tabla de auditoría: ¿por qué “BOLA” no se puede arreglar con un middleware como sí se arreglan los headers?
35.10 Mini reto
Encontrar el endpoint donde un viewer podría hacer algo que no debería.
Ejercicio.
- El 403 delata existencia (enumeración). Fix: lo ajeno responde 404 idéntico a lo inexistente — la regla del cap. 17.
WHERE public_id AND user_idproduce 0 filas en ambos casos → mismo 404, sin pista. - SSRF (Server-Side Request Forgery): el atacante hace que tu servidor visite
http://169.254.169.254/latest/meta-data(la metadata de la nube, con credenciales) o tu red interna. Mitigación: allowlist de hosts permitidos, nunca URLs arbitrarias; si es avatar de terceros, descargar en un worker aislado sin acceso a metadata. - Porque BOLA es semántica: el middleware no sabe que la tarea 41 pertenece al usuario 7 — esa relación vive en la BD y se enforza en la query. Headers/cookies son transporte; ownership es dominio. Por eso se audita por endpoint y se testea como regresión.
Mini reto. Los sospechosos de siempre: PATCH/DELETE de proyectos (¿verifica rol editor/admin o solo membresía?), POST /projects/{id}/members (¿quién puede invitar?), endpoints de exportación (¿viewer exporta datos que no ve?), y cualquier ruta agregada “rápido” sin pasar por requireRole. El patrón del hallazgo: endpoint que verifica quién eres pero no qué puedes hacer aquí.
35.11 Vocabulario técnico del capítulo
Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.
El parámetro es el hueco declarado en la función (def f(x) — x es parámetro); el argumento es el valor concreto que le pasas (f(42) — 42 es argumento). Mismo dato, dos momentos: declaración vs uso.
Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.
Una cookie es un dato que el servidor pone en tu navegador y el navegador devuelve en cada request. httpOnly = JavaScript no puede leerla (el XSS no la roba); Secure = solo viaja por HTTPS.
Un JWT (JSON Web Token) es un token firmado: el servidor lo genera con su secreto y puede verificarlo sin consultar la BD. Ojo: está firmado, no cifrado — cualquiera puede leer su contenido, pero no modificarlo.
Un hash es una función de un solo sentido: contraseña →$2b\(10\)…`. No se puede revertir — por eso las contraseñas se hashean, no se cifran. bcrypt es lento a propósito: fuerza bruta cara para el atacante.
Una firma (HMAC) prueba que un mensaje es auténtico y no fue tocado: se calcula con un secreto sobre el contenido. Los webhooks la usan para que verifiques “esto realmente vino de Stripe”.
Un framework es un esqueleto de aplicación ya decidido: te da la estructura (rutas, validación, errores) y tú llenas la lógica. Diferencia con librería: la librería la llamas tú; el framework te llama a ti.
Un repositorio (repo) es la carpeta del proyecto con todo su historial Git adentro. GitHub/GitLab son servicios que hospedan repos para compartirlos y respaldarlos.
Un proceso es un programa en ejecución: el sistema operativo le da memoria propia y un lugar en la fila del CPU. Tu API es un proceso; Postgres es otro; el navegador es varios. Que “se caiga el proceso” = el programa murió.
La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.
Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.
Una migración es un cambio versionado del esquema: un archivo que dice “crea esta columna” (up) y “bórrala” (down). Son el historial Git de la estructura de la BD — se aplican en orden y no se editan una vez aplicadas.
Un ORM (Object-Relational Mapper) traduce entre objetos del código y filas de la tabla: task.save() en vez de INSERT. Cómodo para el CRUD, peligroso si no sabes qué SQL genera (el N+1 nace ahí).
La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.
La autorización responde “¿qué puedes hacer?”: eres usuario válido (autenticado), pero ¿puedes borrar esta tarea? Se decide por rol o por ownership — y se verifica en cada request, no se recuerda.