Saltar al contenido
Milian Lastre ZequeiraES
security2 min

IDOR y broken object-level authorization: repro y mitigación

Qué es un IDOR, cómo se reproduce cambiando identificadores y cómo se arregla de raíz.

— MLZsecuritywebpentest

Construyo APIs y también las rompo para probar que aguantan. La vulnerabilidad que más veces encuentro en revisiones es la más aburrida de nombrar y la más cara de ignorar: broken object-level authorization (BOLA), conocida clásicamente como IDOR. Es el número 1 del OWASP API Security Top 10. Este writeup es genérico y de laboratorio — nada de targets reales.

Qué es

Un IDOR ocurre cuando un endpoint identifica un objeto por un ID que viene del cliente y verifica autenticación pero no autorización sobre ese objeto. Estás logueado, sí — pero nadie comprueba que el recurso sea tuyo.

Escenario de laboratorio

Una API expone facturas por ID incremental:

GET /api/invoices/1001
Authorization: Bearer <token-de-usuario-A>

Devuelve la factura 1001. El usuario A cambia el ID a 1002 y recibe la factura de otra cuenta. La sesión es válida; la autorización, inexistente.

Reproducción

El patrón es enumerar el identificador. En un lab autorizado:

# Solo en entornos autorizados / CTF. Enumerar un ID numérico ya autenticado.
ffuf -w ids.txt \
     -u https://lab.example/api/invoices/FUZZ \
     -H "Authorization: Bearer $TOKEN" \
     -mc 200 -fr '"error"'

Cada 200 con cuerpo de factura ajena es un hallazgo. IDs secuenciales lo hacen trivial; y usar UUIDs no lo arregla — solo sube el costo de enumerar. Un UUID filtrado en una URL, un log o un Referer sigue siendo directamente accesible.

Causa raíz

El servidor confía en el ID del cliente como si fuera una prueba de propiedad. La consulta es "traé el objeto con este ID", cuando debería ser "traé el objeto con este ID que pertenezca a quien llama".

Mitigación de raíz

Scopear la consulta al dueño y denegar por defecto:

// Vulnerable: autenticado, pero sin autorización a nivel de objeto
app.get("/api/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await db.invoice.findById(req.params.id);
  res.json(invoice); // devuelve cualquier factura
});
 
// Corregido: la consulta se limita al que llama
app.get("/api/invoices/:id", requireAuth, async (req, res) => {
  const invoice = await db.invoice.findFirst({
    where: { id: req.params.id, ownerId: req.user.id },
  });
  if (!invoice) return res.sendStatus(404); // 404, no 403 — no filtres existencia
  res.json(invoice);
});

Checklist de defensa:

  • Autoriza sobre el objeto, no solo autentica la sesión.
  • Scopea toda consulta por dueño/tenant en la capa de datos.
  • 404, no 403, para no revelar qué IDs existen.
  • No confíes en IDs del cliente como prueba de acceso; los UUIDs mitigan, no autorizan.
  • Loguea accesos denegados para detectar enumeración.

La regla es simple y aplica a construir tanto como a auditar: la identidad responde quién sos; la autorización responde a qué tenés derecho. Nunca confundas una con la otra.

Construyamos algo propio

Hablemos