Módulo 4 · Autenticación y seguridad

Lección 22 — OWASP Top 10

Inyección SQL, XSS, CSRF, SSRF y control de acceso roto — con ejemplos reales.

Publicada
En esta lección
  1. Ejercicio 1 — SQLi
  2. Ejercicio 2 — XSS
  3. Ejercicio 3 — SSRF
  4. Ejercicio 4 — check --deploy
  5. Ejercicio 5 — Dependencias
  6. Ejercicio 11 — Los tres del §11
  7. Ejercicio 12 — security.txt y runbook
  8. Resumen del profesor

Ejercicio 1 — SQLi

  1. Con f-string: OR '1'='1 devuelve TODOS los eventos (bypass del filtro); el DROP TABLE (con múltiples statements permitidos) ejecuta y destruye — en Postgres el driver por defecto no permite multi-statement, pero la técnica (stacked queries) existe en otros drivers: la lección es la misma.
  2. Con %s: el payload es un literal de búsqueda inofensivo (title LIKE '%x'' OR ''1''=''1%'): 0 resultados, tabla intacta. La parametrización separa CÓDIGO de DATOS para siempre.
  3. El ORM envía el payload como parámetro del WHERE: el "asiento" llamado '; DROP -- simplemente no existe. (Y de paso: los mensajes de error de la BD jamás llegan al cliente — DEBUG=False.)

Ejercicio 2 — XSS

  1. Con innerHTML: la alerta salta — tu API entregó datos correctos; la vulnerabilidad está en cómo el cliente los interpreta. La superficie XSS de una API es su cliente.
  2. textContent (o el escape del framework): el payload se muestra como texto. Regla registrada: innerHTML prohibido con datos de usuario (¡y coincide con tu regla dicresoft de siempre: cero innerHTML!).
  3. CSP default-src 'self' bloquea scripts inline/event handlers (onerror=): aunque el XSS entre, no ejecuta — defensa en profundidad (con nonce/hash para tus scripts propios).

Ejercicio 3 — SSRF

  1. localhost:6379 devuelve la respuesta (o error) de tu Redis: el atacante usa TU servidor como navegador interno. En nube, 169.254.169.254 entrega credenciales temporales del role de la instancia: game over.
  2. Guard: urlparse para esquema (solo http/https), getaddrinfo para resolver TODAS las IPs del host, rechazo si alguna cae en rangos privados/loopback/link-local, y fetch a la IP resuelta (con Host header) — no a un nombre que puede re-resolver distinto.
  3. DNS rebinding: el atacante controla un dominio que en el check resuelve a IP pública y en el fetch a 127.0.0.1. Resolver y CONECTAR a la misma IP (con Host header) cierra la ventana de re-resolución.

Ejercicio 4 — check --deploy

  1. Típicos: HSTS no configurado, cookies sin Secure, SSL redirect off, SECURE_CONTENT_TYPE_NOSNIFF off, no hay X_FRAME_OPTIONS explícito.
  2. Con el bloque de la lección: los warnings desaparecen (en dev, las cookies Secure rompen el login HTTP local: actívalas con env vars por entorno — la 27).
  3. El browseable API expone formularios de prueba, serializers con opciones y HTML en production: superficie y conveniencia para el atacante. JSON puro en prod (browseable solo si DEBUG).

Ejercicio 5 — Dependencias

  1. pip-audit casi siempre encuentra algo en requirements sin pinnear (CVEs de versiones viejas de Django/Pillow/requests). Las graves: las con exploit público o que afecten endpoints expuestos.
  2. Pinning exacto (reproducibilidad: tu build de hace 6 meses se reconstruye igual) + pip-audit en CI (nueva CVE en dependencia = PR de upgrade automático o al menos alerta). Es el A06 con el mínimo sostenible.

Ejercicio 11 — Los tres del §11

  1. El exploit demostrado: PATCH /api/profile {"is_staff": true} con fields = "__all__" → el usuario se auto-escaló (el test rojo lo documenta). El fix: el serializer del perfil lista EXPLÍCITAMENTE display_name/bio/avatar — is_staff jamás entra por JSON (solo por admin del 21, con audit del 56). El test de regresión envía el campo malicioso y asserts que el modelo no cambió.
  2. El timing medido: la comparación == con strings de 32 chars: los primeros 5-8 caracteres correctos se detectan (~50-200 ns de diferencia acumulada, medible con miles de iteraciones); compare_digest es tiempo constante — el canal lateral cerrado. La lección de las mediciones: el exploit del timing es REAL pero exige estadística; el fix es gratis: no hay excusa.
  3. El typo: requets instala el paquete del typosquatter (con código que lee tus env vars — demostrable en el venv de laboratorio). Con hashes en el lockfile: la instalación falla (el hash no coincide con el del proxy público). El pip-audit de la 41 reporta las CVEs conocidas de las deps reales.

Ejercicio 12 — security.txt y runbook

  1. El security.txt:
text
Contact: mailto:security@ticketflow.dev
Expires: 2027-09-28T00:00:00.000Z
Encryption: https://ticketflow.dev/security/pgp-key.txt
Preferred-Languages: es, en
Policy: https://ticketflow.dev/security/policy
  1. El runbook del filtrado (extracto): (1) triage: confirmar el filtrado con el test del reporte (<24 h); (2) contención: no hay exposición activa explotable si el listado exige auth del 21 — si no la exige: flag que apague el serializer anidado; (3) fix: retirar el campo del serializer + test de contrato que enumera los campos permitidos (35); (4) divulgación: sin brecha confirmada (nadie lo explotó: el audit log del 56 no muestra lecturas anómalas) → changelog; con evidencia de explotación → runbook 72 h del 56; (5) postmortem: la pieza que lo habría cazado: el test de contrato del 35 si hubiera enumerado campos; la acción: añadir "el contract guard verifica el set exacto de campos" al guard del 28.
  2. La respuesta a la pregunta raíz es la que cierra el runbook: sin pieza que lo cazara, el gap es del guard de contratos — y la acción P1 lo añade. Ese bucle (hallazgo → pieza que falta → pieza instalada) es el que convierte cada incidente en fortaleza.

Resumen del profesor

  • El #1 (access control) es disciplina por-PR: matriz + tests de regresión.
  • Inyección: parametriza siempre; el ORM ya lo hace — el riesgo son tus raw/extra/f-strings.
  • XSS vive en el frontend; CSP y textContent/DOMPurify son las capas.
  • SSRF: resolver-antes-de-conectar y rangos privados prohibidos.
  • check --deploy + pip-audit + browseable apagado: la misconfiguration es la más barata de cerrar hoy mismo.

Después: Lección 23 — Secretos, rate limiting y RGPD.