CULTIVA IA — Revisor Senior TypeScript
Revisión de código TypeScript PR #147
NexaFlow SaaS · feat/auth-refactor → main
Veredicto BLOQUEADO 5 críticos · 7 altos
Archivos revisados 5 ficheros TS/TSX
Compilador tsc --noEmit (fallado)
ESLint 14 warnings · 3 errors
Revisor Senior TS Engineer · CULTIVA IA
Fecha 18 jun 2026
5
Severidad
CRITICO
7
Severidad
ALTO
4
Severidad
MEDIO
2
Severidad
INFO
Hallazgos por archivo
📄 src/routes/auth.ts
Critico
Inyección SQL por concatenación directa
Seguridad — SQL Injection auth.ts:42
La query construye el filtro de búsqueda interpolando directamente el valor de req.query.email en el string SQL. Un atacante puede cerrar la cláusula WHERE e inyectar SQL arbitrario — incluyendo DROP TABLE o exfiltración de datos.
Problema — auth.ts:42
const user = await db.raw(`SELECT * FROM users WHERE email = '${req.query.email}'`);
✓ Corrección sugerida
Solución
const user = await db.raw('SELECT * FROM users WHERE email = ?', [req.query.email]); // O mejor aún, usar el query builder de Knex/Prisma: const user = await db('users').where({ email: req.query.email }).first();
Critico
Secreto hardcodeado en el código fuente
Seguridad — Credentials Leak auth.ts:67
La clave JWT secreta está escrita literalmente en el código. Si el repo es público o se filtra, todas las sesiones activas pueden ser falsificadas indefinidamente hasta rotar la clave.
Problema — auth.ts:67
const token = jwt.sign(payload, 'sup3r-s3cr3t-nexaflow-2026', { expiresIn: '7d' });
✓ Corrección sugerida
Solución
const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) throw new Error('JWT_SECRET env var is required'); const token = jwt.sign(payload, JWT_SECRET, { expiresIn: '7d' });
Alto
Uso de any sin justificación en parámetros del handler
Seguridad de tipos — any sin justificación auth.ts:18, 29, 55
Tres funciones de handler reciben req: any, res: any en lugar de los tipos de Express. Esto anula completamente el chequeo de tipos en los handlers más críticos del sistema.
Problema
async function loginHandler(req: any, res: any) {
✓ Corrección sugerida
Solución
import type { Request, Response } from 'express'; async function loginHandler(req: Request<{}, {}, LoginBody>, res: Response) {
Alto
Bloque catch vacío — error silenciado
Manejo de errores — Error swallowed auth.ts:83
El catch que rodea el proceso de registro captura cualquier excepción (BD caída, validación fallida, duplicado) pero no hace nada. El cliente recibe 200 OK aunque la operación haya fallado completamente.
Problema — auth.ts:83
} catch (e) {}
✓ Corrección sugerida
Solución
} catch (error) { logger.error({ error }, 'Registration failed'); res.status(500).json({ error: 'Registration failed' }); return; }
📄 src/routes/leads.ts
Critico
Path Traversal en lectura de archivo CSV
Seguridad — Path Traversal leads.ts:31
El nombre de archivo que envía el cliente se usa directamente en path.join sin sanitizar. Un atacante puede enviar ../../etc/passwd y leer archivos arbitrarios del servidor.
Problema — leads.ts:31
const filePath = path.join('/uploads', req.body.filename); const content = fs.readFileSync(filePath, 'utf8');
✓ Corrección sugerida
Solución
const UPLOAD_ROOT = path.resolve('/uploads'); const resolved = path.resolve(UPLOAD_ROOT, req.body.filename); if (!resolved.startsWith(UPLOAD_ROOT + path.sep)) { return res.status(400).json({ error: 'Invalid filename' }); } const content = await fs.promises.readFile(resolved, 'utf8');
Alto
forEach con callback async — promesas no esperadas
Async correctness — async forEach leads.ts:58
Array.forEach no espera promesas. El endpoint responde 200 antes de que ningún lead haya sido insertado, y los errores de inserción quedan sin capturar.
Problema — leads.ts:58
leads.forEach(async (lead) => { await db('leads').insert(lead); });
✓ Corrección sugerida
Solución
await Promise.all(leads.map((lead) => db('leads').insert(lead))); // Si se necesita inserción secuencial: for (const lead of leads) { await db('leads').insert(lead); }
Alto
JSON.parse sin try/catch sobre input externo
Manejo de errores — JSON sin guardia leads.ts:72
El CSV puede contener campos con JSON anidado que el parser intenta deserializar directamente. Un payload malformado lanza una excepción no capturada que hace caer todo el proceso de importación.
Problema
const meta = JSON.parse(row.metadata);
✓ Corrección sugerida
Solución
let meta: unknown; try { meta = JSON.parse(row.metadata); } catch { meta = {}; /* metadata inválido — continuar sin él */ }
📄 src/utils/fileParser.ts
Critico
Sin validación de schema en datos de entrada externos
Node.js — Falta validación de boundary fileParser.ts:12–40
Los datos del CSV se mapean directamente al objeto de lead e insertan en BD sin ninguna validación de tipos, rangos o campos requeridos. Permite inyectar campos arbitrarios en la BD (mass-assignment) y romper constraints de integridad.
Problema — fileParser.ts:12
return rows.map((row) => row as Lead); // Cast directo sin validar — cualquier campo pasa
✓ Corrección sugerida
Solución con Zod
import { z } from 'zod'; const LeadSchema = z.object({ email: z.string().email(), name: z.string().min(1).max(200), company: z.string().optional(), }); return rows.map((row) => LeadSchema.parse(row));
Medio
Llamada síncrona a fs.readFileSync en request handler
Node.js — Bloqueo del event loop fileParser.ts:8
La lectura síncrona bloquea el event loop de Node.js durante la lectura del archivo. Con archivos CSV de varios MB (frecuentes en importaciones masivas), esto congela el servidor para todas las peticiones concurrentes.
Problema
const raw = fs.readFileSync(filePath, 'utf8');
✓ Corrección sugerida
Solución
const raw = await fs.promises.readFile(filePath, 'utf8');
📄 src/components/LeadTable.tsx
Critico
XSS — datos de usuario en dangerouslySetInnerHTML
Seguridad — Cross-Site Scripting LeadTable.tsx:44
El campo lead.notes (texto libre introducido por usuarios externos) se renderiza sin sanitizar via dangerouslySetInnerHTML. Un atacante puede inyectar <script> que se ejecute en el navegador de cualquier agente que vea el lead.
Problema — LeadTable.tsx:44
<td dangerouslySetInnerHTML={{ __html: lead.notes }} />
✓ Corrección sugerida
Opción A — texto plano (preferible)
<td>{lead.notes}</td>
Opción B — si se necesita HTML, sanitizar antes
import DOMPurify from 'dompurify'; <td dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(lead.notes) }} />
Medio
Uso de índice de array como key en lista dinámica
React — key con índice LeadTable.tsx:38
Con listas reordenables o paginadas, usar el índice como key provoca re-renders incorrectos y puede corromper el estado local de las filas (inputs de edición inline, estados de selección).
Problema
{leads.map((lead, i) => <LeadRow key={i} lead={lead} />)}
✓ Corrección sugerida
Solución
{leads.map((lead) => <LeadRow key={lead.id} lead={lead} />)}
Medio
useEffect para estado derivado — patrón incorrecto
React — Estado derivado en effect LeadTable.tsx:22
El filtro de leads se calcula en un useEffect que escribe en otro estado. Esto provoca dos renders por cada cambio de filtro y un frame de tabla incorrecta visible al usuario.
Problema
const [filtered, setFiltered] = useState(leads); useEffect(() => { setFiltered(leads.filter(bySearch)); }, [leads, search]);
✓ Corrección sugerida
Solución — calcular durante render
const filtered = useMemo( () => leads.filter(bySearch), [leads, search] );
tsconfig.json
Alto
Se ha relajado strict a false y deshabilitado noUncheckedIndexedAccess
Seguridad de tipos — Configuracion del compilador debilitada tsconfig.json:7–8
Deshabilitar strict anula docenas de checks (strictNullChecks, noImplicitAny...). Este cambio fue introducido probablemente para silenciar errores en lugar de corregirlos — constituye deuda de seguridad de tipos para todo el proyecto.
Problema — tsconfig.json:7
"strict": false, "noUncheckedIndexedAccess": false
✓ Corrección sugerida
Solución
"strict": true, "noUncheckedIndexedAccess": true, // Corregir los errores de tipos que afloran — no silenciarlos
Checklist antes del merge
Resolver las 5 vulnerabilidades CRITICAS (SQL injection, path traversal, XSS, secret hardcodeado, mass assignment)
Bloqueante de merge — riesgo inmediato en producción
Corregir los 7 hallazgos ALTOS (any, async forEach, errores silenciados, strict=false...)
Bloqueante de merge — calidad de código y correctitud
Abordar los 4 hallazgos MEDIOS antes de la siguiente iteración
No bloqueante — pero recomendado antes del siguiente PR
Reactivar "strict": true en tsconfig.json y corregir los errores de compilación resultantes
Bloqueante — no se puede mergear con strict=false
Rotar inmediatamente la clave JWT filtrada (sup3r-s3cr3t-nexaflow-2026) aunque el PR no se haya mergeado
URGENTE — cualquier persona con acceso al diff puede impersonar sesiones
Añadir tests unitarios para fileParser.ts y el endpoint de importación
Recomendado — cero cobertura actual en los archivos nuevos
La estructura general de routing con Express es correcta
Sin problemas de arquitectura a nivel macro