daxonix
WRITEUP · 003DXN-WR-2026-003
Writeup · Casos reales anonimizados

Anatomía de un BOLA.

Los 3 patrones de Broken Object Level Authorization que aparecen una y otra vez en plataformas con datos de usuario. Cómo se ven, cómo se prueban, cómo se cierran.

EDITADO POR Daxonix Offensive Security
2026 EDITION · 14 PÁG · CONFIDENCIAL
WRITEUP · DXN-WR-2026-003DAXONIX
CONTEXTO

Por qué BOLA
es la #1.

Broken Object Level Authorization es la primera categoría del OWASP API Security Top 10 desde su primera edición en 2019, y se ha mantenido en la cima cada revisión posterior. La razón es estructural: BOLA es la consecuencia natural de un atajo arquitectónico que se siente correcto cuando lo escribes.

El desarrollador implementa el endpoint GET /api/loans/{id}. Recupera el préstamo de la base de datos. Lo devuelve. El frontend solo solicita IDs que pertenecen al usuario autenticado, así que el flujo "funciona". Los tests pasan porque los tests también solo piden IDs que les pertenecen. La pieza faltante (verificar que el usuario autenticado es el dueño del recurso) parece redundante porque el flujo nunca la ejercita.

Hasta que un atacante con un proxy interceptando peticiones cambia el ID. Y el servidor responde con datos que no debería.

Tres patrones, mismas consecuencias

En auditorías a más de 47 plataformas a lo largo de los últimos años, los hallazgos de BOLA caen consistentemente en tres patrones arquitectónicos. Este writeup los describe con casos reales anonimizados, las técnicas exactas de detección y las remediaciones que funcionan a nivel de framework, no como parches por endpoint.

  1. Patrón 1 · Trust ciego en el ID del path o query. El más común. El servidor obtiene el recurso por el ID provisto por el cliente sin filtrar por owner.
  2. Patrón 2 · ID embebido en el JWT como autoridad. El cliente puede modificar el JWT (porque la firma no se valida o porque el secreto está expuesto) y suplantar otra cuenta.
  3. Patrón 3 · Cross-tenant leak en multi-tenant. La aplicación tiene aislamiento por tenant, pero un endpoint (a veces solo uno) lo olvida.
★ Datos observados

De cada 10 APIs que evaluamos manualmente, 7 tienen al menos un endpoint con BOLA explotable. El tiempo promedio de detección manual es de 30-90 minutos por endpoint vulnerable, dependiendo del tamaño del API.

DAXONIX02
PATRÓN 1 · TRUST CIEGO EN IDDAXONIX
PATRÓN 01

Trust ciego en el ID del
path o query.

FRECUENCIA
~ 50% de APIs
DETECCIÓN
15-30 min/endpoint
SEVERIDAD
CVSS 7.5 - 9.1

El patrón más simple y frecuente. El endpoint recibe un identificador en la URL (path parameter, query string o body) y lo usa directamente para localizar el recurso, sin verificar que pertenezca al usuario autenticado.

Backend vulnerable

# Python · framework genérico
@app.get("/api/loans/{loan_id}")
def get_loan(loan_id: str, user = auth_required()):
    # El servidor sabe quién es el usuario.
    # Pero solo usa el loan_id para buscar.
    loan = db.loans.find_by_id(loan_id)
    if not loan:
        return 404
    return loan  # ← devuelve sin filtrar por owner

Explotación

El atacante autenticado con su propia cuenta consulta primero su propio préstamo y observa el formato del ID. Si es secuencial (entero) prueba IDs adyacentes. Si es UUID los cosecha de otros endpoints (listados paginados, búsquedas, exports).

# Petición original (recurso del atacante)
GET /api/loans/a8f3-c91d-victim-uuid HTTP/1.1
Authorization: Bearer [attacker-token]

# Respuesta: 200 con datos del préstamo de la víctima
{
  "loan_id": "a8f3-c91d-victim-uuid",
  "owner_id": "victim-user-id",
  "amount": 45000,
  "balance": 12300,
  "personal_data": { ... }
}

El servidor responde 200 con el préstamo completo. El campo owner_id en la respuesta delata que el dueño no es el atacante, pero el servidor no realiza esa verificación antes de responder.

DAXONIX03
PATRÓN 1 · TRUST CIEGO EN IDDAXONIX

Caso real anonimizado

FINTECH

Plataforma de crédito al consumo · 1.2M usuariosEl endpoint GET /api/v2/loans/{id} permitía iterar IDs y descargar préstamos de cualquier cliente. Tiempo de detección: 18 minutos. Hallazgos adicionales encadenados: el endpoint GET /api/v2/users/{id}/documents tenía el mismo problema, exponiendo INE, comprobantes de domicilio y declaraciones fiscales. Cerrado pre-launch antes del lanzamiento de la nueva versión del API.

Remediación

La fix arquitectónica es no aceptar el ID del cliente como única referencia para localizar recursos. El servidor debe usar el user_id del token autenticado como filtro obligatorio en toda query.

Backend correcto

@app.get("/api/loans/{loan_id}")
def get_loan(loan_id: str, user = auth_required()):
    loan = db.loans.find_by_id_and_owner(
        loan_id=loan_id,
        owner_id=user.id  # ← filtro obligatorio
    )
    if not loan:
        return 404  # ← mismo código que "no existe"
    return loan

Reglas para evitar reaparición

★ Detección con Burp + Autorize

Configurar Burp con dos sesiones simultáneas (víctima y atacante). La extensión Autorize reenvía cada petición de la sesión víctima usando el token del atacante y compara las respuestas. Marca en rojo cuando la respuesta es idéntica (BOLA confirmado), en verde cuando difiere (control aplicado).

DAXONIX04
PATRÓN 2 · ID EN JWTDAXONIX
PATRÓN 02

ID embebido en el JWT
como autoridad.

FRECUENCIA
~ 20% de APIs
DETECCIÓN
30-90 min
SEVERIDAD
CVSS 8.1 - 9.4

El token JWT incluye un claim como loan_id, account_id o tenant_id. El backend lo lee directamente del token y lo usa para autorizar acceso, asumiendo que el token es inviolable. Si la firma del JWT no se valida correctamente, o si el secreto de firma está expuesto, el atacante modifica el claim y suplanta a otro usuario.

JWT vulnerable

# Token original
{
  "sub": "user_id_12345",
  "loan_id": "loan_98765",  # ← se usa para autorizar
  "role": "borrower",
  "exp": 1710000000
}

Backend vulnerable

@app.get("/api/loans/details")
def get_loan_details(jwt = decode_token()):
    # El servidor confía en el loan_id del token
    loan_id = jwt["loan_id"]
    return db.loans.find_by_id(loan_id)

La explotación combina dos clases de vulnerabilidad: BOLA + JWT validation flaw. El atacante necesita poder modificar el JWT para que el backend acepte un nuevo loan_id. Esto ocurre típicamente porque:

DAXONIX05
PATRÓN 2 · ID EN JWTDAXONIX

Caso real anonimizado

FINTECH

App de préstamos personales · evaluación pre-launchEl JWT incluía el claim loan_id y el backend lo usaba como única referencia para autorizar acceso. La firma se validaba pero con HS256 y un secreto que apareció en una constante del bundle JavaScript público. Forge del token con loan_id arbitrario tomó tres minutos. Acceso total a información financiera personal de cualquier cliente. Críticas remediadas antes de producción mediante migración a RS256 + remoción del claim de autorización del JWT.

Remediación

La regla absoluta: ningún claim del JWT debe ser fuente de autorización para recursos específicos. El JWT autentica al usuario (sub), nada más. La autorización a recursos individuales se calcula en el servidor consultando la base de datos.

Backend correcto

@app.get("/api/loans/details")
def get_loan_details(loan_id: str, jwt = decode_token()):
    # El loan_id viene del request, no del token.
    # El user_id viene del token (autenticación).
    user_id = jwt["sub"]
    loan = db.loans.find_by_id_and_owner(
        loan_id=loan_id,
        owner_id=user_id
    )
    if not loan:
        return 404
    return loan

Hardening del JWT en sí

★ Forge mínimo en jwt_tool

jwt_tool token.jwt -X a prueba alg:none. jwt_tool token.jwt -C -d wordlist.txt brute-force del secreto HS256. jwt_tool token.jwt -X k -pk public.pem intenta confusion HS/RS.

DAXONIX06
PATRÓN 3 · CROSS-TENANT LEAKDAXONIX
PATRÓN 03

Cross-tenant leak
en multi-tenant.

FRECUENCIA
~ 35% de SaaS B2B
DETECCIÓN
1-3 horas
SEVERIDAD
CVSS 8.5 - 9.6

En SaaS multi-tenant, cada cliente (tenant) tiene un espacio aislado de datos. La aplicación implementa el aislamiento mediante un tenant_id que viaja en el JWT, en un header custom o en la URL. La gran mayoría de endpoints aplica el filtro de tenant correctamente. Pero basta que UN endpoint olvide aplicarlo para que un usuario de un tenant pueda acceder a datos de otro.

El típico endpoint olvidado

# Mayoría de endpoints aplican filtro de tenant via middleware
@app.get("/api/companies/{slug}/employees")
def get_employees(slug: str, tenant = current_tenant()):
    # Middleware filtra por tenant.id
    return db.employees.find_for_tenant(tenant.id)

# Pero este endpoint no pasó por el middleware:
@app.get("/api/companies/{slug}/payroll/export")
def export_payroll(slug: str):
    # Solo usa el slug del path, sin validar contra el tenant
    company = db.companies.find_by_slug(slug)
    return generate_csv(company.payroll)

El atacante con cuenta válida en cualquier tenant del SaaS itera slugs de empresas competidoras y descarga sus archivos de nómina. La explotación es directa: el atacante tiene una cuenta legítima, su token es válido, el endpoint responde 200 porque el slug existe.

Variantes que aparecen en el mismo patrón

DAXONIX07
PATRÓN 3 · CROSS-TENANT LEAKDAXONIX

Caso real anonimizado

SAAS B2B

Plataforma multi-tenant de RH · 200+ empresas, 50k usuariosEl endpoint GET /api/companies/{slug}/employees validaba el slug del path pero no contra el tenant del JWT. Cualquier usuario con cuenta válida (incluso plan gratuito de prueba) podía iterar slugs de empresas competidoras y descargar la nómina completa: nombres, salarios, fechas de ingreso, departamentos, datos bancarios. 4 horas de detección manual con dos cuentas de prueba. Cero hallazgos en el escaneo automático que el cliente había contratado seis meses antes.

Remediación arquitectónica

La fix puntual (agregar validación de tenant en el endpoint olvidado) no resuelve el problema. Si un endpoint se olvidó, otros pueden olvidarse también. La solución correcta es imposibilitar el olvido mediante middleware o framework-level enforcement.

Patrón correcto · ORM scoped por tenant

# Middleware aplicado a todas las rutas autenticadas
@app.middleware
def tenant_scope(request, call_next):
    user = request.state.user
    # Toda query subsecuente filtra automáticamente por tenant
    db.set_tenant_scope(user.tenant_id)
    return call_next(request)

# El endpoint no necesita pensar en tenant
@app.get("/api/companies/{slug}/payroll/export")
def export_payroll(slug: str):
    # db.companies ya está scoped al tenant del usuario
    company = db.companies.find_by_slug(slug)  # 404 si es de otro tenant
    return generate_csv(company.payroll)

Reglas operacionales

DAXONIX08
METODOLOGÍA DE DETECCIÓNDAXONIX
METODOLOGÍA

Cómo cazamos BOLA
en cada engagement.

El proceso es disciplinado, no creativo. La creatividad aparece al construir cadenas de explotación; la detección base es metódica.

Fase 1 · Setup

Fase 2 · Mapeo de objetos

Como víctima, generar todos los tipos de recursos que el sistema soporta: préstamos, archivos, mensajes, transacciones. Capturar los IDs de cada uno. Esto es el catálogo de "objetos a probar BOLA".

Fase 3 · Sustitución sistemática

Para cada endpoint del API que recibe un identificador en path, query o body:

  1. Capturar la petición original ejecutada como atacante con un ID propio.
  2. Sustituir el ID por uno de la víctima (capturado en Fase 2).
  3. Reenviar. Observar respuesta.
  4. 200 con datos completos = BOLA confirmado. 403 o 404 = control aplicado. 200 con datos parciales o filtrados = revisión manual del response body.

Fase 4 · Variantes

Los endpoints que no tienen BOLA en el método principal pueden tenerlo en métodos secundarios:

DAXONIX09
HERRAMIENTAS Y AUTOMATIZACIÓNDAXONIX

Herramientas que aceleran la auditoría

Burp Autorize

Extensión gratuita disponible en BApp Store. Configurar el token "low privilege" (atacante) en la extensión. Burp re-ejecuta automáticamente cada petición de la sesión normal usando el token configurado y compara las respuestas. Marca con código de color: rojo (probable BOLA), verde (control aplicado), amarillo (revisar manualmente).

Burp AuthMatrix

Más potente que Autorize: define múltiples roles y ejecuta la matriz completa de permisos por endpoint. Útil cuando la aplicación tiene 3+ niveles de privilegio (user, manager, admin, super-admin).

Custom Burp macros para token rotation

En aplicaciones con tokens de corta duración o que requieren re-autenticación, configurar una macro que renueva el token antes de cada petición de la sesión víctima. Sin esto, la mitad de los tests fallan por sesión expirada.

Scripts en Python con requests

Cuando la lógica de la aplicación requiere encadenar peticiones (autenticarse, navegar, llegar al endpoint, ejecutar BOLA test), escribir scripts custom es más eficiente que automatizar en Burp. Plantillas reusables por engagement.

Lo que NO funciona

★ Tiempo total realista

Para un API con 30-50 endpoints autenticados, una auditoría completa de BOLA + variantes (PUT, DELETE, bulk) toma entre 8 y 16 horas con dos cuentas y herramientas adecuadas. Audits ejecutados en menos tiempo no son creíbles.

DAXONIX10
REMEDIACIÓN ARQUITECTÓNICADAXONIX
REMEDIACIÓN

Cómo prevenir BOLA
a nivel de framework.

Las fixes puntuales por endpoint son frágiles: cada nuevo endpoint que se añade es una oportunidad de olvidar el control. La remediación efectiva es arquitectónica.

Capa 1 · Repository pattern con scope obligatorio

Los repositorios de datos no exponen métodos genéricos de búsqueda. Solo métodos que requieren el identificador del usuario o tenant. Compilación o linting falla si un endpoint llama a métodos sin scope.

# MAL · método peligroso disponible
def find_loan(loan_id):
    return db.query("SELECT * FROM loans WHERE id = ?", loan_id)

# BIEN · scope obligatorio en la firma
def find_loan_for_user(loan_id, user_id):
    return db.query(
        "SELECT * FROM loans WHERE id = ? AND owner_id = ?",
        loan_id, user_id
    )

Capa 2 · Row-level security en la base de datos

PostgreSQL RLS (y equivalentes en MySQL via VIEWs, MongoDB con $expr) aplica el filtro de tenant a nivel de la DB engine. Aún si el código de la aplicación tiene un bug, la DB rechaza la query si el filtro no incluye el tenant correcto.

Capa 3 · Middleware de tenant scope

En aplicaciones multi-tenant, middleware que ejecuta antes de cada handler establece el tenant context. Las queries posteriores incluyen automáticamente el filtro. Eliminar el contexto al finalizar la petición.

Capa 4 · Tests de regresión cross-account

Suite automatizada que ejecuta cada endpoint del API con la cuenta de "atacante" usando IDs de "víctima". El test pasa solo si el endpoint responde 403 o 404, nunca 200. Falla en CI bloquea el merge.

Capa 5 · Audit log y alertas

Cuando un usuario intenta acceder a un recurso ajeno (incluso si el control lo bloquea), el evento se loggea. Métricas y alertas detectan enumeración activa: un usuario que dispara 50 eventos de "no autorizado" en una hora es probable atacante en proceso.

DAXONIX11
CASOS REALES · MUESTRADAXONIX
CASOS

Patrones observados
en producción.

Selección de hallazgos BOLA documentados en engagements recientes. Todos anonimizados; severidades y patrones reales.

FINTECH

Plataforma de crédito al consumo · 12 hallazgos totales. 3 críticas BOLA: préstamos cross-customer, exportación de documentos KYC ajenos, modificación de status de préstamos de otros usuarios. Cerrado pre-launch.

BANKING

Banca digital · onboarding KYC + 2FA · 8 altas. ATO vía password reset que aceptaba cualquier email para iniciar el flujo y revelaba si la cuenta existía con base en el código de respuesta. Encadenado con BOLA en el endpoint de verificación de identidad.

SAAS B2B

Plataforma multi-tenant · módulo wholesale · 9 hallazgos. BOLA cross-tenant en el endpoint de export de payroll. JWT signing key expuesta en el bundle del frontend permitía forge de tokens administrativos. Refactor completo del IAM antes del próximo lanzamiento.

FINTECH

Portal corporativo + app móvil · 11 hallazgos. Bypass de root detection en Android, session reuse entre dispositivos, BOLA en endpoint de consulta de transacciones que no validaba ownership cuando el cliente enviaba el account_id en el body.

INSURTECH

Plataforma de pólizas · APIs públicas y privadas · 2 críticos. BOLA en la API de consulta de pólizas que aceptaba policy_id arbitrarios. Refactor de IAM completo y cambio del modelo de autorización a row-level security en PostgreSQL.

BANKING

APIs de transferencias y pagos enterprise · 1 crítico race condition. 4 altas BOLA: una en el endpoint de reversa de transferencia, dos en endpoints de export para reconciliación contable, una en bulk operation sobre cuentas que mezclaba ownership.

DAXONIX12
POR QUÉ MANUALDAXONIX
CIERRE TÉCNICO

Por qué BOLA exige
auditoría manual.

BOLA es la categoría de vulnerabilidad donde la diferencia entre auditoría automática y manual es más evidente. Un escáner de DAST puede detectar inyección SQL con cierta confiabilidad porque el patrón es local: una comilla en un parámetro produce un comportamiento detectable en la respuesta inmediata. BOLA no es local. Requiere construir un grafo de objetos válidos en la aplicación, ejecutar peticiones con cuentas distintas y comparar respuestas con criterio sobre qué constituye un acceso indebido.

Tres elementos que ningún escáner puede automatizar al nivel de un humano:

  1. Generar objetos válidos como víctima. La cuenta de prueba "víctima" debe interactuar con la aplicación como un usuario real para producir recursos válidos cuyos IDs el atacante luego sustituirá. Esto requiere navegación stateful que un escáner no replica fielmente.
  2. Inferir el formato del ID. UUIDs requieren cosechado desde otros endpoints. IDs secuenciales requieren razonar sobre rangos válidos. IDs codificados (base64, hash) requieren entender la codificación. Un escáner trata todo input como string opaco.
  3. Distinguir respuesta legítima de fuga. Un endpoint puede responder 200 con datos parciales filtrados (solo campos públicos del recurso ajeno) en lugar de 403. Esto es un BOLA parcial que solo se detecta inspeccionando el response body comparativamente. Un escáner mira solo el código de status.

El gap medido

En clientes que habían contratado escaneos automáticos comerciales antes de nuestro engagement, encontramos en promedio 4 hallazgos críticos o altos de BOLA por proyecto que el escáner había clasificado como "limpios". Las herramientas no son malas; son aplicables al subset de problemas que pueden detectarse sin contexto. BOLA no está en ese subset.

El otro lado de la moneda: un pentester con criterio dedicado a un API durante 8-16 horas encontrará BOLA si existe. La técnica está documentada, las herramientas están disponibles, el patrón es repetible. La barrera no es técnica, es organizacional: presupuesto, tiempo, prioridad. Cuando esos recursos se asignan, los hallazgos aparecen.

DAXONIX13
daxonix

Auditamos BOLA
como parte de cada proyecto.

Si tu API maneja recursos por usuario y nunca pasó por una auditoría manual con dos cuentas en paralelo, hay buenas probabilidades de que al menos un endpoint tenga BOLA explotable.

EMAIL
CALENDLY
calendly.com/daxonix
LINKEDIN
/in/isaac-milan-soto