🔍
Variant Analysis Report CRÍTICO AGF-SEC-2024-007
AgendaFlow API · Node.js/TypeScript · BOLA/IDOR · 18 Jun 2026 · Analista: CULTIVA IA Security
Repo: agendaflow-api
LOC analizadas: 18.247
Controladores: 34
9
Variantes encontradas
5
Confirmadas explotables
3
Requieren revisión
1
Falso positivo
6
Recursos afectados
🐛 Vulnerabilidad Original root cause confirmado
Tipo
BOLA / IDOR
CVSS
9.1 Crítico
Endpoint
GET /api/v1/appointments/:id
Archivo
src/routes/appointments.ts:47
Root cause: El controlador recupera cualquier recurso por su :id de URL sin validar que req.user.id === resource.userId. Cualquier usuario autenticado puede acceder a datos de otro usuario conociendo el ID.
TypeScript — appointments.ts:47 // ❌ VULNERABLE — sin verificación de ownership router.get('/:id', authenticate, async (req, res) => { const appointment = await Appointment.findById(req.params.id); // req.user.id nunca se compara con appointment.userId if (!appointment) return res.status(404).json({ error: 'Not found' }); return res.json(appointment); // ← devuelve datos de cualquier usuario }); // ✅ FIX correcto router.get('/:id', authenticate, async (req, res) => { const appointment = await Appointment.findById(req.params.id); if (!appointment) return res.status(404).json({ error: 'Not found' }); if (appointment.userId.toString() !== req.user.id) { return res.status(403).json({ error: 'Forbidden' }); } return res.json(appointment); });
🔎 Progresión de Búsqueda 5 pasos — ripgrep + semgrep
1
Patrón exacto — match único
Verificar que sólo devuelve la instancia original conocida.
rg -n "Appointment.findById(req.params.id)" src/ → 1 match: src/routes/appointments.ts:47
2
Generalizar el modelo — todas las entidades
Sustituir Appointment por cualquier modelo Mongoose.
rg -n "\.findById\(req\.params\." src/ → 14 matches en 8 archivos
3
Semgrep — patrón taint: findById sin check ownership
Buscar cualquier findById / findOne / findByPk en rutas donde req.user.id no aparece en el scope del handler.
semgrep --config resources/semgrep/javascript.yaml src/routes/ → 9 matches confirmados (5 high confidence, 3 medium, 1 FP)
4
Ampliar a métodos relacionados — UPDATE y DELETE
El mismo patrón en operaciones de escritura tiene impacto mayor.
rg -n "findByIdAndUpdate|findByIdAndDelete|save\(\)" src/routes/ → 6 matches adicionales (incluidos en los 9 anteriores)
5
Clasificar — FP rate final: 11% (1/9) ✓ aceptable
Clasificación manual de cada match. Tasa de FP por debajo del umbral del 50%.
Confirmadas (8/9)
Falso positivo (1/9)
📊 Evolución del Patrón de Búsqueda
Ver.PatrónHerramientaMatchesTPFPFP%
v1Appointment.findById(req.params.id)ripgrep1100%
v2\.findById\(req\.params\.ripgrep148643%
v3findById sin ownership check en scopesemgrep98111%
v4findById + findOne + findByPk + savesemgrep+rgDesc. — FP 61%61% ✗
v3 seleccionado como patrón final — precisión 89%, cobertura completa de variantes confirmadas.
🚨 Variantes Encontradas 8 hallazgos clasificados
#1
IDOR en perfil de paciente — lectura no autorizada
CRÍTICO Confirmado
src/routes/patients.ts:83 · GET /api/v1/patients/:id
Alta confianza HIPAA impact Datos PII
El controlador devuelve el perfil completo del paciente (nombre, DNI, historial médico, dirección) sin verificar que el solicitante tenga relación con ese paciente. Un atacante con cualquier ID de paciente puede extraer datos clínicos sensibles.
const patient = await Patient.findById(req.params.id); // ← req.user nunca se valida contra patient.assignedDoctorId ni patient.userId return res.json(patient);
Alcanzable desde internet
Input controlado por atacante
Sin sanitización
#2
IDOR en facturas — acceso y descarga no autorizados
CRÍTICO Confirmado
src/routes/invoices.ts:31 · GET /api/v1/invoices/:id
Alta confianza Datos financieros
Las facturas incluyen datos de pago (últimos 4 dígitos de tarjeta, monto, servicio). Cualquier usuario puede descargar el PDF de factura de otro usuario. Combinado con enumeración secuencial de IDs, permite exfiltración masiva de datos financieros.
const invoice = await Invoice.findById(req.params.id).populate('patient'); const pdfBuffer = await generateInvoicePDF(invoice); res.setHeader('Content-Type', 'application/pdf'); res.send(pdfBuffer);
Alcanzable desde internet
IDs secuenciales (MongoDB ObjectId predecible)
Sin rate limiting
#3
IDOR en informes médicos — modificación no autorizada
ALTO Confirmado
src/routes/reports.ts:109 · PUT /api/v1/reports/:id
Alta confianza Escritura Integridad de datos
El endpoint de actualización de informes no verifica ownership. Un atacante puede modificar o corromper el historial clínico de cualquier paciente. Impacto en integridad de datos críticos de salud.
const report = await Report.findByIdAndUpdate( req.params.id, req.body, // ← body sin sanitizar { new: true } );
Alcanzable desde internet
Input controlado por atacante
Modificación directa de BBDD
#4
IDOR en notas de staff — acceso entre profesionales
MEDIO Confirmado
src/routes/staff-notes.ts:62 · GET /api/v1/staff-notes/:id
Confianza media Acceso horizontal
Un miembro del staff puede acceder a las notas privadas de otro profesional de la clínica. Impacto limitado a datos internos pero viola la política de confidencialidad.
Alcanzable internamente
Input controlado
Requiere token de staff
#5
IDOR en pagos — consulta de estado de transacciones
ALTO Confirmado
src/routes/payments.ts:44 · GET /api/v1/payments/:id
Alta confianza Datos financieros
El estado de pago incluye proveedor de pago, monto y metadatos de Stripe. No hay validación de que el pago pertenezca al usuario solicitante.
Alcanzable desde internet
Input controlado
Sin datos de tarjeta completos
#UbicaciónDescripciónSeveridadConfianzaEstado
6 src/routes/appointments.ts:91 DELETE cita — cancelación de cita ajena Alto Alta Confirmado
7 src/routes/profiles.ts:55 PATCH perfil usuario — modificar datos de otro Medio Media En revisión
8 src/routes/notifications.ts:28 PATCH notificación — marcar como leída ajena Bajo Media En revisión
FP-1 src/routes/config.ts:17 Configuración global — no tiene userId por diseño N/A Falso positivo
📈 Distribución de Riesgo
Crítico
2
Pacientes + Facturas
Impacto regulatorio (HIPAA/GDPR)
Alto
3
Informes + Pagos + DELETE citas
Medio
2
Staff notes + Perfil usuario
Bajo
1
Notificaciones
Falso positivo
1
Config global (sin owner)
Total analizado
34
controladores REST
26% afectados
✅ Recomendaciones
Acción Inmediata (72h)
1. Parchear findings #1 y #2 (pacientes + facturas) — riesgo regulatorio
2. Parchear #3 y #5 (informes + pagos) — datos críticos
3. Parchear #6 (DELETE citas) — integridad de operaciones
4. Extraer helper assertOwnership(resource, req.user) reutilizable
5. Añadir tests de seguridad con IDs cruzados
Preventivo (30 días)
1. Implementar capa de autorización centralizada (middleware)
2. Adoptar patrón de repositorio con ownership embebido
3. Integrar regla Semgrep en CI/CD (bloquear PR)
4. Auditoría completa de endpoints admin
5. Formación de equipo en OWASP BOLA (A01:2021)
⚙️ Regla Semgrep lista para CI/CD
YAML — .semgrep/bola-check.yaml rules: - id: missing-ownership-check patterns: - pattern: | $MODEL.findById(req.params.$ID) - pattern-not: | ... req.user.id ... message: "BOLA: findById sin verificación de ownership. Añadir comparación con req.user.id" languages: [javascript, typescript] severity: ERROR metadata: category: security cwe: CWE-639 owasp: A01:2021