| Archivo | Severidad | Consumidores afectados | Riesgo |
|---|---|---|---|
src/utils/auth.ts |
CRÍTICO | Todos los endpoints protegidos (12 rutas) | Cambio en verifyToken() afecta toda la auth de la app |
src/db/queries/users.ts |
CRÍTICO | PatientService, AdminService, ReportService | SQL injection expone tabla completa de pacientes (PII) |
src/api/routes/admin.ts |
CRÍTICO | Todos los usuarios del sistema | Reset masivo de contraseñas sin autenticación |
src/services/csvParser.ts |
MEDIO | ImportController | Sin validación de tipo/tamaño de archivo; path traversal |
package.json (multer, csv-parse, nodemailer) |
MEDIO | Build pipeline, bundle | 3 dependencias nuevas; revisar CVEs en nodemailer |
src/db/migrations/20260612_add_import_log.sql |
BAJO | DB schema | Sin migración DOWN; no reversible en rollback |
src/api/routes/admin.ts:12 — STRIPE_SECRET_KEY = 'sk_live_4eR8mNpQ2vXw...'. Credencial de producción hardcodeada. URGENTE: revocar en dashboard de Stripe inmediatamente.src/db/queries/users.ts:15 — WHERE name LIKE '%${term}%'. Permite extraer o corromper toda la tabla patients.src/api/routes/admin.ts:3 — POST /api/admin/reset-all-passwords con TODO: add auth check later. Cualquier atacante puede resetear las contraseñas de todos los usuarios.src/utils/auth.ts:10 — process.env.JWT_SECRET || 'fallback-secret-123'. Si JWT_SECRET no está configurado en producción, todos los tokens firmados con el fallback son válidos para cualquiera que conozca la cadena.src/api/routes/admin.ts:7 — md5(Math.random().toString()). MD5 no es apto para hashing de contraseñas. Usar bcrypt o argon2.src/api/routes/patients.ts — multer acepta cualquier archivo. Riesgo de subir scripts maliciosos. Añadir fileFilter para validar text/csv y tamaño máximo.__proto__ ni constructor[ en el diff.dangerouslySetInnerHTML ni innerHTML = en los archivos modificados del PR.sk_live_) está presente en texto plano en el repositorio. Cualquier persona con acceso al repo puede usarla para realizar cargos fraudulentos o acceder a datos de clientes de pago.
- const STRIPE_SECRET_KEY = 'sk_live_4eR8mNpQ2vXw...'; + // 1. Revocar AHORA en dashboard.stripe.com/apikeys + // 2. Añadir a .env (nunca al repositorio) + const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY; + if (!STRIPE_SECRET_KEY) throw new Error('STRIPE_SECRET_KEY not set');
term se interpola directamente en la query SQL. Un atacante puede inyectar ' OR '1'='1 para extraer todos los registros, o '; DROP TABLE patients; -- para destruir datos de PII de pacientes (RGPD/HIPAA).
- const query = `SELECT * FROM patients WHERE name LIKE '%${term}%' OR email = '${term}'`; - return db.execute(query); + return db.query( + 'SELECT * FROM patients WHERE name ILIKE $1 OR email = $2', + [`%${term}%`, term] + );
POST /api/admin/reset-all-passwords no verifica ningún token ni rol. Un atacante sin credenciales puede resetear las contraseñas de todos los usuarios del sistema y recibir las nuevas por email (que controla el servidor de email). Vectorio de toma completa de la aplicación.
- router.post('/api/admin/reset-all-passwords', async (req, res) => { - // TODO: add auth check later + router.post('/api/admin/reset-all-passwords', + requireAuth, + requireRole('SUPER_ADMIN'), + async (req, res) => { + // audit log obligatorio para operaciones destructivas + await auditLog.record({ action: 'bulk_reset', actor: req.user.id });
JWT_SECRET no está definida en el entorno (error de configuración en staging/prod), la app firma tokens con 'fallback-secret-123'. Cualquier atacante que conozca esta cadena puede forjar tokens válidos para cualquier usuario.
- return jwt.verify(token, process.env.JWT_SECRET || 'fallback-secret-123'); + const secret = process.env.JWT_SECRET; + if (!secret) throw new Error('JWT_SECRET env var is not configured'); + return jwt.verify(token, secret);
Math.random() no es criptográficamente seguro.
- const hash = md5(Math.random().toString()); + import { randomBytes } from 'crypto'; + import argon2 from 'argon2'; + const tempPassword = randomBytes(16).toString('hex'); + const hash = await argon2.hash(tempPassword);
db.findOne() y patientService.create() son llamadas dentro de un bucle for...of. Con un CSV de 1.000 pacientes, esto genera 2.000 queries secuenciales. Usar operaciones batch.
- for (const patient of results) { - await db.findOne({ email: patient.email }); - await patientService.create(patient); - } + await patientService.bulkUpsert(results, { onConflict: 'email' });
import_logs pero no define el rollback (DROP TABLE import_logs). Si hay que hacer un rollback del deploy, la migración quedará aplicada y causará inconsistencias.
+ -- DOWN + DROP TABLE IF EXISTS import_logs;
fileFilter para aceptar solo text/csv y un límite de tamaño (ej. 5MB) para evitar DoS por uploads masivos.imported_at evitará full scans cuando la tabla crezca./admin/reset-all-passwords parece scope creep. ¿Pertenece a otro ticket? Si no tiene ticket propio, crear NUTRI-224 antes de mergear.csvParser.test.ts fue añadido. Sin tests para:auth.ts sin tests tras modificaciónimport_logs tiene tipos correctos, valor por defecto sensato y timestamp automático. Solo falta el DOWN.
csvParser.test.ts cubre el happy path y el encoding UTF-8, que es un buen inicio para un colaborador nuevo.