Subagente IA Revisor de Código Senior

Revisión de Código — NutriFlow SaaS

PR #47 — feature/auth-api Autor: @carlos.dev (junior) Base: main 18 Jun 2026 · 14:32 UTC 3 archivos · +218 / -12 líneas
🚫

BLOQUEADO — No mergear hasta resolver issues CRITICAL

Se encontraron 2 vulnerabilidades críticas de seguridad y 3 issues HIGH de calidad. Este PR no puede pasar a producción en su estado actual.

Archivos revisados

src/api/auth.ts
src/api/users.ts
src/components/UserList.tsx

Resumen de hallazgos

2
Critical
BLOQUEA merge
3
High
resolver antes
2
Medium
recomendado
3
Low
opcional
Critical 2 hallazgos · deben resolverse antes del merge
SQL Injection por concatenación de strings en query de login
src/api/auth.ts:34
Critical

El endpoint de login construye la query SQL concatenando directamente el email del usuario sin parametrizar. Un atacante puede enviar email = "' OR '1'='1" y autenticarse sin contraseña, o ejecutar queries arbitrarias.

Escenario de fallo: POST /api/auth/login con body {"email": "' OR 1=1--", "password": "x"} devuelve el primer usuario de la BD con sesión válida. No hay ORM ni validación de esquema que lo intercepte.

Codigo vulnerable
src/api/auth.ts
async function loginUser(email: string, password: string) { const query = `SELECT * FROM users WHERE email = '${email}'`; const user = await db.query(query); const query = 'SELECT * FROM users WHERE email = $1'; const user = await db.query(query, [email]); if (!user) throw new AuthError('Invalid credentials');
Solucion

Usar siempre queries parametrizadas ($1, $2... con pg). Nunca interpolar variables en strings SQL. Considerar usar un query builder (Drizzle, Knex) que parametrice automáticamente.

API key de produccion hardcodeada en el fuente
src/api/auth.ts:8
Critical

La clave del servicio de email (SendGrid) está hardcodeada como string literal. Esta clave ya está en git history y debe revocarse inmediatamente, independientemente del fix que se aplique ahora.

Escenario de fallo: Cualquier persona con acceso al repositorio (o que pueda leer el historial git) obtiene acceso total al servicio de email. Riesgo inmediato de abuso para enviar phishing o incurrir en costes elevados.

src/api/auth.ts
const SENDGRID_KEY = "SG.xK9mP2qR_abc123defXYZ_real_key_here"; const sgClient = new SendGrid(SENDGRID_KEY); const sgClient = new SendGrid(process.env.SENDGRID_API_KEY!);
Accion inmediata requerida

1. Revocar la key en el panel de SendGrid ahora. 2. Crear nueva key y añadir a variables de entorno (.env.local, no versionado). 3. Añadir .env* a .gitignore. 4. Hacer git filter-branch o BFG para purgar el historial.

High 3 hallazgos · resolver antes del merge
Query sin LIMIT en endpoint de listado de usuarios
src/api/users.ts:67
High

El endpoint GET /api/users hace SELECT * FROM users sin paginación. Con 10.000 pacientes en la tabla, cada petición a este endpoint cargará toda la tabla en memoria, causando OOM o timeouts en producción.

src/api/users.ts
const users = await db.query('SELECT * FROM users WHERE clinic_id = $1', [clinicId]); const limit = Math.min(Number(req.query.limit) || 50, 100); const offset = Number(req.query.offset) || 0; const users = await db.query( 'SELECT id, name, email, created_at FROM users WHERE clinic_id = $1 ORDER BY created_at DESC LIMIT $2 OFFSET $3', [clinicId, limit, offset] );
useEffect con dependencia faltante — stale closure en UserList
src/components/UserList.tsx:28
High

El useEffect que carga los usuarios no incluye clinicId en su array de dependencias. Si el dietista cambia de clínica en el mismo session, el componente no re-fetching y muestra los pacientes de la clínica anterior.

src/components/UserList.tsx
useEffect(() => { fetchPatients(clinicId); }, []); useEffect(() => { fetchPatients(clinicId); }, [clinicId]);
Mensaje de error interno enviado al cliente en auth failures
src/api/auth.ts:89
High

Los errores del bloque catch son reenviados directamente al cliente con error.message. Errores de base de datos o stack traces con rutas internas del servidor quedan expuestos al navegador. Facilita el fingerprinting de la infraestructura.

src/api/auth.ts
} catch (error: any) { res.status(500).json({ error: error.message, stack: error.stack }); } } catch (error) { logger.error('Login failed', { error, userId: req.body?.email }); res.status(500).json({ error: 'An error occurred. Please try again.' }); }
Medium 2 hallazgos · recomendado resolver
Key de lista usando index del array en pacientes reordenables
src/components/UserList.tsx:54
Medium

Se usa el index i como key en el listado de pacientes. Como la lista puede ordenarse por nombre/fecha, React no podrá rastrear correctamente los elementos causando renders incorrectos o pérdida del estado local de cada card.

src/components/UserList.tsx
{patients.map((p, i) => <PatientCard key={i} patient={p} />)} {patients.map(p => <PatientCard key={p.id} patient={p} />)}
3 console.log de debug sin eliminar antes del merge
src/api/users.ts:23, :45, :78
Medium

Hay tres console.log de desarrollo activos que imprimen objetos de usuario (incluyendo emails) en el log del servidor de producción. Esto puede violar GDPR al persistir datos personales en logs no controlados.

src/api/users.ts
console.log('user data:', user); console.log('DEBUG query result:', rows); console.log('Clinic users:', JSON.stringify(users)); logger.debug('User fetched', { userId: user.id }); // sin PII
Low 3 hallazgos · opcionales / buenas practicas
3 TODOs sin referencia a ticket de Jira/Linear
src/api/auth.ts:12, src/api/users.ts:99, :134
Low

Hay TODOs sueltos (// TODO: add rate limiting, // TODO: pagination) sin número de ticket. En 3 meses nadie sabrá cuáles están resueltos. Añadir referencia o crear ticket y linkarlo.

Funcion updateUser sin JSDoc en API publica exportada
src/api/users.ts:112
Low

La función export async function updateUser() tiene 4 parámetros opcionales con semántica no obvia. Una doc mínima evitaría preguntas al autor.

Variables data, res2, tmp con nombres poco descriptivos
src/api/users.ts:34, :56, :78
Low

Naming genérico en contextos no triviales. Renombrar a patientRecord, updatedUser, etc. facilita el mantenimiento.