Cuando el UUID no es una autorización: recibos de clientes expuestos encadenando dos IDOR
La ruta que mostraba el recibo de un cliente se protegía con un UUID de 128 bits. Adivinarlo era inviable, así que el equipo dejó de intentarlo y buscó dónde estaba escrito.
Resumen
La ruta del recibo no comprobaba quién pedía el documento: daba por buena la posesión del UUID. Es la confusión de fondo entre un identificador difícil de adivinar y una autorización.
Un identificador impredecible solo protege mientras nadie pueda leerlo en otro sitio. Aquí se podía leer en dos sitios, y ninguno de los dos era la aplicación: en los servicios que archivan direcciones públicas, donde habían quedado registradas URL de recibo con su identificador dentro, y en una segunda referencia directa insegura, esta con identificadores secuenciales, que los devolvía uno a uno.
Por separado, cada pieza habría pasado por un hallazgo de severidad media. Encadenadas convierten un documento aislado en la lectura de los recibos de toda la cartera de clientes. El fallo está corregido.
Detalles técnicos
La ruta del recibo
Con el UUID en la mano, el recibo se servía sin sesión ni comprobación de titularidad:
GET /clientes/recibos/8f14e45f-ceea-467a-9a1b-3d6c2f0b17ac HTTP/1.1
Host: [OBJETIVO]
HTTP/1.1 200 OK
Content-Type: application/pdf
Content-Disposition: inline; filename="recibo-[REF].pdf"El documento incluía nombre, dirección fiscal, identificador tributario, concepto e importe del cliente.
Primera fuente: las direcciones ya estaban archivadas
El enlace al recibo se comparte por correo, se pega en un ticket, se abre desde una red corporativa. Cada uno de esos pasos deja rastro fuera de la organización. Buscando en servicios públicos que conservan direcciones (archivos históricos de la web, plataformas de análisis de enlaces y los propios buscadores) aparecieron direcciones de recibo con su UUID dentro, y seguían sirviendo el documento:
UUID_RE = r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"
RUTA = "/clientes/recibos/"
# Ninguna de estas fuentes es del objetivo, y ninguna pide autenticacion
for fuente in (archivo_historico, analisis_de_enlaces, buscadores):
for url in fuente.buscar(dominio=OBJETIVO, contiene=RUTA):
uuid = findall(UUID_RE, url)
if uuid and get(RUTA + uuid[0]).status == 200:
guardar(uuid[0]) # el documento seguia sirviendoseSegunda fuente: un IDOR clásico que devolvía el UUID
Una funcionalidad secundaria de la propia aplicación aceptaba un identificador numérico y secuencial y respondía con la ficha del cliente, con el UUID del recibo dentro. Esa segunda referencia directa convertía la extracción parcial en enumeración completa:
for customer_id in range(1, 100000):
ficha = get(f"/[FUNCIONALIDAD]?id={customer_id}")
if ficha.ok:
uuid = ficha.json["receipt_uuid"]
pdf = get(f"/clientes/recibos/{uuid}") # 200 OKLa cadena
Las tres piezas, en orden, describen el recorrido completo:
1. URL archivadas por terceros -> UUID de recibo validos, sin tocar el objetivo
2. IDOR con id secuencial -> UUID de cualquier cliente, uno a uno
3. Ruta del recibo sin authz -> PDF completo de cada clienteUn identificador impredecible reduce la probabilidad de acertar, no sustituye a la comprobación de permisos. En cuanto ese identificador viaja dentro de una URL, deja de estar bajo tu control: queda en el historial del navegador, en la cabecera de referencia, en los registros de los intermediarios y en los archivos públicos de la web. A partir de ahí ya no es un secreto, y la ruta que dependía de él queda abierta.
Impacto
- Lectura de los recibos de toda la cartera de clientes, sin autenticarse
- Datos fiscales y de contacto expuestos: nombre, dirección, identificador tributario e importes
- Enumeración completa por identificadores secuenciales de la funcionalidad secundaria
- Direcciones conservadas por terceros, fuera del alcance de cualquier corrección hecha en el servidor
- Brecha de confidencialidad con datos personales y económicos de terceros
Remediación
- Autorización en cada ruta. Comprobar que la sesión que pide el recibo es la titular del documento, con UUID o sin él.
- Identificadores que no viajen de más. No devolver el UUID del recibo en respuestas que no lo necesiten.
- Enlaces de un solo uso. Si el documento se comparte, firmar el enlace y darle caducidad en lugar de dejarlo permanente.
- Dar por pública toda URL compartida. Solicitar la retirada de las direcciones ya archivadas y asumir que las demás siguen ahí.
- Autorización también en la funcionalidad secundaria. El IDOR con identificador secuencial era la mitad de la cadena.
Referencias
- OWASP API Security Top 10: API1:2023 BOLA
- CWE-639: Authorization Bypass Through User-Controlled Key
- IDOR en API REST con exposición masiva de datos personales, donde la recomendación fue sustituir los identificadores secuenciales por UUID. Esta ficha explica por qué ese cambio, por sí solo, no cierra el fallo.
Más análisis
Las cadenas no salen de un escáner: salen de encadenar a mano lo que por separado parece menor
Cuéntanos qué activos quieres auditar. Respondemos en menos de 24 horas.