agendaflow-apiGET /api/v1/appointments/:idsrc/routes/appointments.ts:47:id de URL sin validar que req.user.id === resource.userId. Cualquier usuario autenticado puede acceder a datos de otro usuario conociendo el ID.
// ❌ 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);
});
rg -n "Appointment.findById(req.params.id)" src/
→ 1 match: src/routes/appointments.ts:47
Appointment por cualquier modelo Mongoose.rg -n "\.findById\(req\.params\." src/
→ 14 matches en 8 archivos
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)
rg -n "findByIdAndUpdate|findByIdAndDelete|save\(\)" src/routes/
→ 6 matches adicionales (incluidos en los 9 anteriores)
| Ver. | Patrón | Herramienta | Matches | TP | FP | FP% |
|---|---|---|---|---|---|---|
| v1 | Appointment.findById(req.params.id) | ripgrep | 1 | 1 | 0 | 0% |
| v2 | \.findById\(req\.params\. | ripgrep | 14 | 8 | 6 | 43% |
| v3 | findById sin ownership check en scope | semgrep | 9 | 8 | 1 | 11% ✓ |
| v4 | findById + findOne + findByPk + save | semgrep+rg | Desc. — FP 61% | — | — | 61% ✗ |
const patient = await Patient.findById(req.params.id);
// ← req.user nunca se valida contra patient.assignedDoctorId ni patient.userId
return res.json(patient);
const invoice = await Invoice.findById(req.params.id).populate('patient');
const pdfBuffer = await generateInvoicePDF(invoice);
res.setHeader('Content-Type', 'application/pdf');
res.send(pdfBuffer);
const report = await Report.findByIdAndUpdate(
req.params.id,
req.body, // ← body sin sanitizar
{ new: true }
);
| # | Ubicación | Descripción | Severidad | Confianza | Estado |
|---|---|---|---|---|---|
| 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 |
assertOwnership(resource, req.user) reutilizablerules:
- 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