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 resultantesBloqueante — no se puede mergear con strict=false
Rotar inmediatamente la clave JWT filtrada (
sup3r-s3cr3t-nexaflow-2026) aunque el PR no se haya mergeadoURGENTE — 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