ruta backend / módulo 4

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:

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


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:

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


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):


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            # ¿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):

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:


Checklist de dominio M4

Marcás cuando podés explicar (Feynman) + aplicar:

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.