Ejercicio 1 — SQLi
- Con f-string:
OR '1'='1devuelve 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. - 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. - 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
- 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.
- 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!).
- 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
localhost:6379devuelve 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.- Guard:
urlparsepara esquema (solo http/https),getaddrinfopara 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. - 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
- Típicos: HSTS no configurado, cookies sin Secure, SSL redirect off, SECURE_CONTENT_TYPE_NOSNIFF off, no hay X_FRAME_OPTIONS explícito.
- 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).
- 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
- 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.
- 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
- El exploit demostrado:
PATCH /api/profile {"is_staff": true}confields = "__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_staffjamá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ó. - 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_digestes 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. - El typo:
requetsinstala 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). Elpip-auditde la 41 reporta las CVEs conocidas de las deps reales.
Ejercicio 12 — security.txt y runbook
- El security.txt:
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- 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.
- 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.