Nota de origen: este cheatsheet documenta un script y flujo de trabajo REALES, creados y ejecutados por Websec Argus en un pentest autorizado a una plataforma SaaS multi-tenant. Se anonimizó completamente — sin dominio,
org_id, tokens ni datos reales.
Muchas plataformas SaaS multi-tenant exponen su REST API con un patrón /api/v1/orgs/{org_id}/<recurso>.
El error más común no está en un solo endpoint, sino en la SUPERFICIE completa: basta con tener un
token JWT válido de UN tenant (incluso una cuenta de prueba propia) para descubrir que decenas de
endpoints no verifican que org_id en la URL coincida con el tenant real del token.
Flujo de trabajo real
- Levantar el mapa de endpoints sin autenticación: muchas de estas plataformas exponen
GET /api/v1/openapi.jsonpúblicamente (sinAuthorization) — es el mapa completo de la superficie de ataque, generado por el propio framework (FastAPI/Django REST/NestJS con Swagger). Si existe, es el primer paso: da la lista completa de recursos{org_id}-scoped a probar. - Obtener UN token válido de UN tenant — cuenta de prueba propia otorgada por el cliente para el engagement, o un usuario de bajo privilegio.
- Escribir un script que enumere TODOS los endpoints listados en el openapi.json, sustituyendo
{org_id}por: (a) el org_id propio (control — debe funcionar), (b) un org_id ajeno adivinado o filtrado en otra respuesta (el verdadero test de IDOR/BOLA). - Automatizar la descarga completa, no solo un endpoint suelto: un solo IDOR aislado ("puedo ver el perfil de otro org") suele subestimarse en el informe. Descargar TODOS los recursos accesibles (miembros, contactos, transacciones, consumo/facturación, invitaciones) en un solo script demuestra el impacto real: exfiltración completa de un tenant ajeno, no solo un campo.
- Separar recursos "globales" de recursos "por org": algunos endpoints (
/templates,/voicesen un ejemplo real de plataforma de voz IA) no llevanorg_iden absoluto y devuelven datos compartidos entre TODOS los tenants con solo tener sesión — otro hallazgo, distinto del IDOR pero igual de relevante para el informe (falta de segmentación por diseño, no solo falla de autorización puntual).
Esqueleto de script (patrón real, generalizado)
import json, os, urllib.request
API = "https://<target>/api/v1"
def req(url, token):
r = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"})
try:
with urllib.request.urlopen(r, timeout=20) as resp:
return resp.status, resp.read().decode(errors="replace")
except urllib.error.HTTPError as e:
return e.code, e.read().decode(errors="replace")
def exfil_org(org_id, token, out_dir):
endpoints = {
"org.json": f"{API}/orgs/{org_id}",
"members.json": f"{API}/orgs/{org_id}/members",
"contacts.json": f"{API}/orgs/{org_id}/contacts?page_size=500",
"consumption.json": f"{API}/orgs/{org_id}/consumption",
"transactions.json": f"{API}/orgs/{org_id}/consumption/transactions",
# ... poblar dinámicamente esta lista desde openapi.json
}
os.makedirs(out_dir, exist_ok=True)
for name, url in endpoints.items():
code, body = req(url, token)
if code == 200:
open(os.path.join(out_dir, name), "w").write(body)
La clave metodológica no es el código (es HTTP básico) sino la disciplina de cobertura total:
generar la lista de endpoints a partir del propio openapi.json del objetivo en vez de probar
"a mano" 3 o 4 rutas — así no se subestima el alcance real del IDOR.
Por qué importa para un pentest: en Websec Argus este patrón corresponde a lo que
idor_scanner automatiza de forma genérica, pero cuando el objetivo expone
su propio openapi.json, vale la pena generar el script de exfiltración dirigido en el momento —
cubre en minutos endpoints que un scanner genérico no conoce de antemano (rutas específicas del
dominio de negocio del cliente, ej. /promises, /phone-numbers en una plataforma de cobranza).
Mitigación real a recomendar: middleware de autorización que valide org_id de la URL contra
el org_id embebido en el JWT/sesión en TODOS los endpoints (no verificación ad-hoc por handler),
y separación explícita de recursos verdaderamente globales vs. recursos que deberían ser
org_id-scoped pero no lo son.
Ver también: [[indice-de-casos-reales-de-brechas-publicas-por-vector#APIs sin autenticación / IDOR|casos públicos de IDOR/API]], [[metodologia-de-pentesting-de-apis-rest-y-graphql-enumeracion|metodología de pentesting de APIs REST/GraphQL]], [[cheatsheet-de-dos-por-abuso-de-limites-ausentes-creacion-masiva-e-importacion-sin-limite|DoS por límites ausentes]] (mismo engagement, mismo tenant).