JWT no es autorización
Un token válido te dice quién sos, no qué podés hacer. La diferencia entre autenticación y autorización, y por qué confundirlas produce IDOR.
Antes de empezar necesitás
- Saber qué es una API HTTP y un endpoint
- Idea básica de qué es un token de sesión
Al terminar vas a poder
- Distinguir autenticación (authn) de autorización (authz)
- Leer el payload de un JWT y entender qué garantiza y qué no
- Reconocer un IDOR en el diseño de un endpoint
- Escribir el chequeo de ownership que falta
Un equipo agrega JWT, mete un middleware de login y da la seguridad por cerrada. El problema es que JWT resuelve una pregunta —¿quién sos?— y la gente cree que resolvió otra —¿esto lo podés hacer?—. No es lo mismo, y la confusión tiene nombre: IDOR.
Qué es (y qué no es) un JWT
Un JWT es un texto con tres partes separadas por puntos: header.payload.signature. El payload son claims (afirmaciones) en JSON, codificados en base64. La firma garantiza que nadie alteró esos claims.
# El payload de un JWT no está cifrado, solo codificado en base64.
echo "$JWT" | cut -d '.' -f2 | base64 -d 2>/dev/null Vas a ver algo así:
{
"sub": "user_42",
"email": "valen@example.com",
"role": "user",
"exp": 1735689600
}
Pegá un JWT (usá uno de juguete, nunca uno real de producción) y mirá cómo se separa en header, payload y firma. Todo se decodifica en tu navegador: nada se envía a ningún lado.
Decodificador de JWT
Pegá un token y mirá qué dice por dentro. Un JWT está firmado, no cifrado: cualquiera lee el contenido. Acá no verificamos la firma y el token nunca sale de tu navegador.
El error: confiar en la identidad sin chequear el recurso
Mirá este endpoint. El token es válido, el usuario está autenticado. ¿Está bien?
GET /api/invoices/{id}
función obtener_factura(request, id):
usuario = validar_jwt(request.headers["Authorization"]) # authn ✓
si usuario es inválido:
responder 401
factura = db.buscar_factura(id) # ← sin authz
responder 200, factura
Está roto. El usuario user_42 tiene un token perfectamente válido para su factura 100. Pero nada le impide pedir la 101:
# Mismo token válido, otro id. Si responde 200, es IDOR.
curl -s -H "Authorization: Bearer $JWT" https://api.example.com/api/invoices/101 El fix: chequear ownership
El arreglo no es criptografía nueva. Es una condición que muchas veces falta:
GET /api/invoices/{id}
función obtener_factura(request, id):
usuario = validar_jwt(request.headers["Authorization"]) # authn ✓
si usuario es inválido:
responder 401
factura = db.buscar_factura(id)
si factura es nula:
responder 404
si factura.owner_id != usuario.sub: # authz ✓
responder 404 # 404, no 403: no revelamos que existe
responder 200, factura
El criterio para revisar APIs
JWT, OAuth o sesiones resuelven el transporte de identidad. La autorización es trabajo aparte y va por endpoint y por recurso. Una tabla mínima para revisar:
endpoint authn ownership/authz notas
GET /invoices/{id} sí owner_id == sub 404 si no es tuyo
POST /invoices sí rol puede crear validar input
DELETE /invoices/{id} sí owner o rol admin log de la acción
GET /admin/users sí rol == admin rate limit
Tu API no es segura por usar JWT. Es segura cuando, además de saber quién entra, decide bien qué puede tocar.
Lo que practicás en este lab
Llevátelo a tu repo si querés, pero no es obligatorio: es tu aprendizaje.
- Writeup de 2 párrafos: por qué un token válido no alcanza
- Pseudocódigo del endpoint vulnerable y del corregido
- Una tabla chica: por endpoint, qué chequeo de autorización aplica
Reto
Tomá el endpoint vulnerable de abajo y agregá el chequeo de ownership. Escribí en dos líneas qué request maliciosa deja de funcionar después del fix.
Resolvelo y escribí dos líneas explicando qué pasó. Con eso lo fijás.