src/routes/patients.js
id, from, to y limit se interpolan directamente en la cadena SQL sin parametrizar ni sanitizar. Un atacante autenticado puede enviar ?from=2020-01-01' OR '1'='1 y obtener todos los registros de todos los pacientes de la clínica, o ?from=x'; DROP TABLE nutrition_records;-- para destruir datos.
db.query('SELECT * FROM nutrition_records WHERE patient_id = $1', [parseInt(id, 10)]). Validar id como entero positivo, from/to como fechas ISO 8601, limit como entero en rango [1, 1000].
:id. Cualquier usuario con sesión válida puede incrementar el id en la URL y acceder al historial completo de cualquier otro paciente. Ejemplo: /patients/1/records, /patients/2/records… hasta barrer toda la base de datos.
req.user.clinicId coincide con el clinicId del paciente consultado antes de ejecutar la query. Ejemplo: const patient = await getPatient(id); if (patient.clinicId !== req.user.clinicId) return res.status(403).json({error:'Forbidden'});
console.log(`[DEBUG] CSV export for patient ${id}: ${csv}`) vuelca el CSV completo (todos los registros nutricionales del paciente) en los logs de aplicación. En cualquier entorno con log aggregation (Datadog, CloudWatch, Loggly), estos datos de salud quedan almacenados sin cifrar, accesibles a cualquier persona con acceso a logs.
console.log completamente. Si se necesita auditoría de exportaciones, registrar solo metadatos: logger.info({ event:'csv_export', patientId: id, requestedBy: req.user.id, recordCount: records.rows.length })
async no tiene try/catch. Si getDbConnection() falla (pool agotado, timeout, red caída) o db.query() lanza una excepción, Express devuelve un stack trace completo al cliente que incluye nombres de tablas, estructura de la base de datos y versión del servidor.
try/catch y responder con res.status(500).json({error:'Internal server error'}). Logear el error interno sin exponerlo al cliente.
Content-Disposition en export CSV
Content-Disposition: attachment; filename="patient-{id}-records.csv". El navegador renderizará el CSV en pantalla en lugar de descargarlo. Además, el CSV no tiene fila de cabeceras con nombres de columnas, lo que hace el archivo inutilizable sin contexto.
res.set('Content-Disposition', `attachment; filename="patient-${id}-records.csv"`) y añadir cabecera con Object.keys(records.rows[0]).join(',') como primera línea.
LIMIT 100 no hay forma de obtener registros más allá de los 100 más recientes. Un paciente con historial de 2 años tiene ~730 registros y el nutricionista no puede acceder a los antiguos. Sin offset/cursor y sin metadatos de paginación en la respuesta.
getDbConnection() — posible leak de conexión
db.release() ni db.end() después del query. Dependiendo de cómo implemente el pool getDbConnection(), esto puede agotar el pool de conexiones bajo carga. Sin el try/catch (W-01), si el query falla, la conexión nunca se devuelve al pool.
GET /patients/1/records?from=2020-01-01' UNION SELECT * FROM users-- ejecuta una query arbitraria. No hay ninguna validación en el camino.
for id in 1..10000 y cosecha el historial médico completo de todos los pacientes de la plataforma. Sin rate limiting, sin auth check por recurso.
UnhandledPromiseRejection con el stack trace completo. El atacante aprende la estructura interna del sistema.
limit=99999999 en la URL fuerza una query que devuelve millones de registros, satura memoria del servidor y provoca OOM. No hay cota máxima de limit.
text/csv pero sin cabecera de columnas. Cuando lo probé en Postman no sabía qué significaba cada valor. ¿Es id, patient_id, recorded_at, calories, protein...? No hay forma de saberlo sin mirar el schema de la tabla.
records.rows. ¿Qué campos tiene cada objeto? ¿Hay campos calculados? ¿Por qué SELECT * y no campos explícitos?
nutrition_records y posiblemente otras tablas vía UNION attacks.
:id. El endpoint asume que si el usuario está autenticado, puede ver cualquier paciente.
Este endpoint tiene tres vulnerabilidades críticas independientes, cada una suficiente por sí sola para bloquear el merge: inyección SQL sin parametrizar, ausencia total de control de acceso (IDOR) y volcado de datos médicos en logs. El PR fue desarrollado con buenas intenciones pero sin conocimiento de seguridad básica en APIs web.
El problema más urgente es la SQL injection: una vez desplegado, cualquier usuario autenticado puede exfiltrar la base de datos completa en minutos. El código "funciona en local con paciente ID 1" porque el happy path nunca activa las vulnerabilidades.
No mergear bajo ninguna circunstancia hasta resolver C-01, C-02 y C-03. El deadline de mañana no justifica exponer datos de salud de pacientes a una brecha trivialmente explotable.