M4 — Security
Outcome del módulo: cerrar un hueco real de seguridad y documentar el threat model. Pensar como atacante sobre tu propio código.
Principio transversal: seguridad = default, no feature. No se agrega al final; se diseña desde el inicio.
Concepto 1 — AuthN vs AuthZ (autenticación vs autorización)
Idea núcleo: autenticación = quién sos. Autorización = qué podés hacer. Son dos problemas distintos y se confunden todo el tiempo.
Entender (capa 1)
Texto: primero el sistema verifica identidad (login, token) = AuthN. Después decide si esa identidad tiene permiso para la acción/recurso = AuthZ. Un token válido (autenticado) no implica permiso (autorizado) para todo.
Visual:
flowchart LR
REQ[request] --> AUTHN{¿quién sos?<br/>token válido?}
AUTHN -->|no| R401[401 Unauthorized]
AUTHN -->|sí| AUTHZ{¿podés hacer ESTO?<br/>rol/permiso}
AUTHZ -->|no| R403[403 Forbidden]
AUTHZ -->|sí| OK[ejecuta]
Video: OAuth 2.0 vs OpenID Connect (OIDC): What IT Admins Need to Know — Omnissa
Ejemplo (el bug clásico — falta AuthZ):
# ❌ autentica pero NO autoriza: cualquier usuario logueado borra cualquier post
@app.delete("/posts/{post_id}")
def delete_post(post_id, user=Depends(current_user)): # solo AuthN
db.delete_post(post_id)
# ✅ verifica que el post es del usuario (AuthZ)
@app.delete("/posts/{post_id}")
def delete_post(post_id, user=Depends(current_user)):
post = db.get_post(post_id)
if post.owner_id != user.id:
raise HTTPException(403) # autorización
db.delete_post(post_id)
Fijar (capa 2)
Nota atómica:
- Front: diferencia entre 401 y 403.
- Back: 401 Unauthorized = no autenticado (no sé quién sos / token inválido). 403 Forbidden = autenticado pero sin permiso para esto. AuthN falla → 401; AuthZ falla → 403.
Feynman: “AuthN es el guardia que revisa tu documento en la entrada. AuthZ es que tu entrada VIP no te deja pasar a la cabina del DJ. Estás adentro, pero no a todo.”
Aplicar (capa 3)
Tomá un endpoint que solo autentica y agregale autorización a nivel de recurso (que el dueño sea el usuario). Probá acceder con otro usuario y verificá el 403.
Límites
- La AuthZ a nivel de objeto (IDOR: acceder al recurso de otro cambiando el ID) es el hueco más común; nunca confíes solo en el ID que manda el cliente.
Concepto 2 — JWT y tokens bien hechos
Idea núcleo: un JWT es un token firmado que lleva claims (quién, qué, hasta cuándo). Firmado ≠ cifrado: cualquiera lee el contenido, pero no puede falsificarlo sin la clave.
Entender (capa 1)
Texto: JWT = header.payload.signature (base64). El server firma con su secreto; al recibirlo, valida la firma. Ventaja: stateless (no consultar DB por sesión). Riesgos: no se puede revocar fácil (por eso expiración corta + refresh tokens), y el payload es legible (no metas secretos ahí).
Visual:
flowchart LR
LOGIN[login OK] --> SIGN[server firma JWT<br/>con secreto] --> CLIENT[cliente guarda token]
CLIENT -->|Authorization: Bearer| API[cada request] --> VERIFY{¿firma válida?<br/>¿no expiró?}
VERIFY -->|sí| OK[procesa]
VERIFY -->|no| R401[401]
Video: What are JWT Access token & Refresh token and why we need them? — Rahul Ahire
Ejemplo:
import jwt
from datetime import datetime, timedelta, timezone
def make_token(uid):
payload = {"sub": str(uid), "exp": datetime.now(timezone.utc) + timedelta(minutes=15)} # sub debe ser string (spec JWT; PyJWT ≥2.10 lo valida)
return jwt.encode(payload, SECRET, algorithm="HS256") # firma
def verify(token):
return jwt.decode(token, SECRET, algorithms=["HS256"]) # valida firma + exp
Fijar (capa 2)
Nota atómica:
- Front: ¿por qué no guardar datos sensibles en el payload de un JWT?
- Back: el payload está solo firmado (base64), no cifrado — cualquiera con el token lo lee. La firma evita manipulación, no lectura. Datos sensibles → server-side, no en el token.
Feynman: “Un JWT es un sobre lacrado con tu sello: todos ven la carta a través del sobre transparente, pero nadie puede cambiarla sin romper el lacre. No metas tu PIN en una carta transparente.”
Aplicar (capa 3)
Emití un access token (15 min) + refresh token. Verificá que un token expirado se rechaza y que uno con la firma alterada también.
Límites
- Access token corto + refresh token para renovar; el refresh sí se guarda/revoca server-side.
- Nunca
algorithm=none; fijá el algoritmo permitido al validar (ataque de algoritmo).
Concepto 3 — Hashing de contraseñas
Idea núcleo: las contraseñas se guardan hasheadas con un algoritmo lento y con salt (argon2/bcrypt), nunca en texto plano ni con hashes rápidos (MD5/SHA).
Entender:
# ❌ nunca: texto plano, o hash rápido (crackeable a millones/seg)
# ✅ argon2 / bcrypt: lento a propósito + salt único por password
from argon2 import PasswordHasher
ph = PasswordHasher()
hash_ = ph.hash("password-del-user") # guardás esto
ph.verify(hash_, intento) # valida en login
Nota atómica: Front: ¿por qué bcrypt/argon2 y no SHA-256 para passwords? Back: SHA es rápido → un atacante prueba miles de millones/seg. bcrypt/argon2 son lentos a propósito y con salt por password, haciendo el crackeo inviable y bloqueando rainbow tables. Feynman: “Un hash rápido es una cerradura que se abre en un segundo probando llaves. argon2 es una caja fuerte que tarda 3 segundos por intento: para vos son 3s, para el que prueba millones es una eternidad.” Aplicar: hasheá y verificá un password con argon2. Medí cuánto tarda un verify (debe ser perceptible, ~decenas de ms). Límites: el costo (work factor) se calibra: alto = más seguro pero más CPU por login.
Concepto 4 — OWASP Top 10 aplicado a APIs Python
Idea núcleo: la lista de las vulnerabilidades más comunes. Conocer el ataque para defender.
Entender (los que más pegan en backend):
- Injection (SQL): nunca concatenar input en queries → usar parámetros.
# ❌ f"SELECT * FROM users WHERE name='{name}'" ← SQL injection # ✅ cursor.execute("SELECT * FROM users WHERE name=%s", (name,)) - Broken Access Control (IDOR): acceder a recursos de otro cambiando el ID → validar ownership (ver Concepto 1).
- SSRF: tu server hace requests a URLs que da el usuario → validar/allowlist destinos.
- Security Misconfiguration: debug on en prod, CORS abierto, headers faltantes.
- Cryptographic Failures (A02:2021, ex “Sensitive Data Exposure”): datos sensibles sin cifrar. Logs con tokens/PII caen en A09 Security Logging Failures; secretos en el repo, en gestión de secretos.
Video: OWASP Top 10 Explained (Still Relevant in 2025) — The Treatment
Nota atómica: Front: ¿cómo se previene SQL injection? Back: queries parametrizadas (placeholders
%s/:param), nunca concatenar/f-string el input del usuario en el SQL. El ORM ayuda, pero raw SQL mal hecho reabre el hueco. Feynman: “SQL injection es dejar que el cliente escriba parte de tu orden a la cocina: si le permitís texto libre, escribe ‘y además regalá toda la caja’. Los parámetros son un formulario cerrado: solo llena los campos, no reescribe la orden.” Aplicar: creá un endpoint vulnerable a SQL injection a propósito, explotalo, después arreglalo con parámetros. Límites: el Top 10 es piso, no techo; cambia con los años (revisar la versión vigente).
Concepto 5 — Supply chain: escaneo de dependencias y código
Idea núcleo: buena parte del riesgo no está en tu código sino en tus dependencias. Escanealas automáticamente.
Entender:
pip-audit: revisa tus deps contra CVEs conocidos.bandit: SAST para Python — detecta patrones inseguros en tu código (uso deeval,subprocesscon shell, secretos hardcodeados).semgrep: reglas de patrones sobre el código, personalizables.
pip-audit # ¿alguna dependencia con vulnerabilidad conocida?
bandit -r . # ¿patrones inseguros en mi código?
Nota atómica: Front: ¿qué revisa pip-audit vs bandit? Back: pip-audit escanea las dependencias contra CVEs publicados (riesgo de terceros); bandit es SAST sobre tu propio código Python (patrones inseguros). Complementarios: uno mira afuera, otro adentro.
Feynman: “pip-audit revisa si los ingredientes que compraste tienen retiro sanitario. bandit revisa si vos cocinaste con las manos sucias.”
Aplicar: corré pip-audit y bandit -r . sobre un proyecto; arreglá al menos un hallazgo. Metelos al pipeline CI (M3) para que corran en cada push.
Límites: los scanners tienen falsos positivos; hay que triagearlos, no ignorarlos en bloque.
Concepto 6 — Secretos, least privilege y rate limiting
Idea núcleo: tres hábitos que cierran la mayoría de los huecos operacionales.
Entender (compacto):
- Secrets management: nunca en código/imagen/YAML; en un secret manager, inyectados por env, con rotación. Un secreto commiteado = comprometido para siempre (queda en el historial de git).
- Least privilege: cada credencial/servicio con el mínimo permiso necesario. Un user con permisos amplios = superficie de ataque enorme si se filtra.
- Rate limiting: limitar requests por cliente/IP para frenar abuso, brute force y scraping (token bucket — ver M2/Patterns).
flowchart LR
REQ[requests] --> RL{¿bajo el límite?}
RL -->|sí| OK[procesa]
RL -->|no| R429[429 Too Many Requests]
Nota atómica: Front: si commiteo un secreto y lo borro en el commit siguiente, ¿estoy a salvo? Back: no — queda en el historial de git y en cualquier clon/fork. Hay que rotar el secreto (invalidarlo y generar uno nuevo); borrarlo del código no alcanza.
Feynman: “Un secreto en git es una llave que dejaste en una foto publicada. Borrar la foto no sirve: ya la vieron y la copiaron. Tenés que cambiar la cerradura (rotar).”
Aplicar: poné rate-limit (ej: 100 req/min por IP) a un endpoint público y verificá el 429. Mové una credencial del .env a un secret manager.
Límites: rate limiting muy agresivo bloquea usuarios legítimos; se calibra por endpoint (login estricto, lectura pública más laxo).
Concepto 7 — Threat modeling
Idea núcleo: antes de shippear, preguntarte sistemáticamente “¿cómo rompo esto?”. Pensar como atacante.
Entender:
- Método simple (STRIDE-lite): por cada entrada/recurso, ¿qué puede Spoofing (suplantar), Tampering (alterar), Repudiation, Information disclosure, DoS, Elevation of privilege?
- Salida = una lista de amenazas + cómo las mitigás (o el riesgo que aceptás).
Nota atómica: Front: ¿qué es threat modeling y cuándo se hace? Back: analizar sistemáticamente cómo se podría atacar un sistema (por entrada/recurso: suplantar, alterar, filtrar, DoS, escalar privilegios) y decidir mitigaciones. Se hace en diseño, no después del incidente.
Feynman: “Es contratar a un ladrón para que camine tu casa antes de mudarte y te diga por dónde entraría. Mejor saberlo con planos que con la casa ya robada.”
Aplicar: escribí un
threat-model.mdcorto para un endpoint: listá 3 formas de atacarlo y la mitigación de cada una. Límites: no es exhaustivo ni una sola vez; se revisita cuando el sistema cambia.
Checklist de dominio M4
Marcás cuando podés explicar (Feynman) + aplicar:
- AuthN vs AuthZ (401 vs 403, IDOR)
- JWT bien hecho (firma, exp, refresh, no secretos en payload)
- Hashing de passwords (argon2/bcrypt vs hash rápido)
- OWASP Top 10 (injection, access control, SSRF, misconfig)
- Supply chain (pip-audit, bandit, semgrep)
- Secretos + least privilege + rate limiting
- Threat modeling
Salida verificable del módulo: un endpoint con AuthN + AuthZ a nivel de recurso, passwords con argon2, rate-limit activo, pip-audit/bandit corriendo en CI sin hallazgos críticos, y un threat-model.md con 3 amenazas y sus mitigaciones.