Revisión de Seguridad de Rama

feature/auth-api → main
📦 LeadFlow SaaS 🗓 16 Jun 2026 🔍 6 archivos revisados 📐 OWASP Top 10 2021
🚫

NO APTO PARA MERGE

Se requiere resolver 2 críticos y 3 altos antes del merge.

2
Críticos
3
Altos
3
Medios
2
Bajos
10
Total
📂 Fase 1 — Archivos Modificados en la Rama
Archivo Líneas +/- Superficie de ataque Estado
src/auth/login.js +87 / -12 Autenticación, queries SQL, JWT emit ⛔ 3 issues
src/db/queries.js +143 / -0 SQL raw, parámetros usuario, ORM bypass ⛔ 2 issues
src/utils/crypto.js +56 / -8 Hashing, generación de tokens, secretos ⚠️ 2 issues
src/routes/leads.js +94 / -3 Export CSV, autorización, paginación ⚠️ 2 issues
src/auth/middleware.js +48 / -31 Validación JWT, CORS, cabeceras ⚠️ 1 issue
src/routes/users.js +62 / -14 CRUD usuarios, gestión errores ✓ Minor only
🔴 Hallazgos Críticos 2 críticos
#01
SQL Injection directa en consulta de login
src/db/queries.js:34 · src/auth/login.js:52
Crítico
Problema
El email del usuario se interpola directamente en la query SQL sin sanitizar ni usar consultas parametrizadas. Un atacante puede bypassar la autenticación o extraer toda la base de datos.
Evidencia — código vulnerable
// src/db/queries.js:34 const getUserByEmail = async (email) => { const sql = `SELECT * FROM users WHERE email = '${email}'`; // ⛔ INTERPOLACIÓN DIRECTA return db.query(sql); };
Fix sugerido
// Usar consultas parametrizadas const getUserByEmail = async (email) => { return db.query( `SELECT id, email, role, password_hash FROM users WHERE email = $1`, [email] // ✅ PARÁMETRO ENLAZADO ); };
Vector de ataque
# Payload ejemplo: bypassea login sin contraseña email: admin@leadflow.io' OR '1'='1 # Query resultante: SELECT * FROM users WHERE email = 'admin@leadflow.io' OR '1'='1'
Referencias
OWASP A03:2021 Injection CWE-89 CVSSv3: 9.8 Critical
#02
JWT_SECRET hardcodeado en el código fuente
src/utils/crypto.js:8 · src/auth/login.js:71
Crítico
Problema
La clave secreta de JWT está hardcodeada como string literal en el código. Cualquier persona con acceso al repositorio puede firmar tokens válidos para cualquier usuario, incluyendo administradores.
Evidencia — código vulnerable
// src/utils/crypto.js:8 const JWT_SECRET = 'leadflow-secret-2024'; const signToken = (payload) => jwt.sign(payload, JWT_SECRET, { algorithm: 'HS256' });
Fix sugerido
// .env JWT_SECRET=<openssl rand -base64 64> // src/utils/crypto.js const JWT_SECRET = process.env.JWT_SECRET; if (!JWT_SECRET) throw new Error('JWT_SECRET no definido');
Referencias
OWASP A02:2021 Cryptographic Failures CWE-321 CWE-798
🟠 Hallazgos Altos 3 altos
#03
IDOR en exportación de leads — acceso a datos de otros clientes
src/routes/leads.js:41
Alto
Problema
El endpoint GET /api/leads/export acepta un parámetro client_id en la query string sin verificar que el usuario autenticado tenga acceso a ese cliente. Cualquier usuario puede exportar leads de cualquier cuenta.
Evidencia
// src/routes/leads.js:41 router.get('/export', auth, async (req, res) => { const { client_id, format } = req.query; // ⛔ SIN verificar req.user.client_id === client_id const leads = await getLeadsByClient(client_id); res.csv(leads); });
Fix sugerido
if (req.user.client_id !== client_id && req.user.role !== 'admin') { return res.status(403).json({ error: 'Forbidden' }); }
Referencias
OWASP A01:2021 Broken Access Control CWE-639 IDOR
#04
Sin rate limiting en endpoint de login — fuerza bruta posible
src/auth/login.js:18
Alto
Problema
El endpoint POST /auth/login no tiene ningún mecanismo de rate limiting ni bloqueo tras intentos fallidos. Un atacante puede realizar ataques de fuerza bruta o credential stuffing sin restricción.
Fix sugerido
// Usar express-rate-limit + Redis const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 min max: 5, // 5 intentos store: new RedisStore({ client: redis }), skipSuccessfulRequests: true }); router.post('/login', loginLimiter, loginHandler);
Referencias
OWASP A07:2021 Auth Failures CWE-307
#05
Contraseñas hasheadas con MD5 en lugar de bcrypt/argon2
src/utils/crypto.js:22
Alto
Problema
La función hashPassword usa MD5, un algoritmo criptográficamente roto y sin salt. Los hashes MD5 pueden ser crackeados en segundos con tablas rainbow.
Evidencia
const hashPassword = (pwd) => crypto.createHash('md5').update(pwd).digest('hex'); // ⛔ MD5, sin salt, sin iteraciones
Fix sugerido
const bcrypt = require('bcrypt'); const hashPassword = (pwd) => bcrypt.hash(pwd, 12); // ✅ Con salt automático
Referencias
OWASP A02:2021 CWE-916 NIST SP 800-63B
🟡 Hallazgos Medios 3 medios
#06
Contraseñas logueadas en claro en consola de error
src/auth/login.js:63
Medio
Evidencia
catch (err) { console.error(`Login failed for ${email}: ${password}`); }
Fix
console.error(`Login failed for ${email}`); // Nunca loguear la contraseña
#07
JWT emitidos sin expiración (expiresIn ausente)
src/utils/crypto.js:12
Medio
Problema
Los tokens JWT no tienen fecha de expiración. Si un token es robado, es válido indefinidamente hasta que el secreto sea rotado.
Fix
jwt.sign(payload, JWT_SECRET, { algorithm: 'HS256', expiresIn: '15m' // + refresh token });
#08
CORS configurado con wildcard — acepta cualquier origen
src/auth/middleware.js:9
Medio
Evidencia
app.use(cors({ origin: '*' })); // ⛔ Cualquier dominio puede llamar a la API
Fix
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(','), credentials: true }));
🟢 Hallazgos Bajos 2 bajos
#09
Mensajes de error exponen stack traces al cliente
src/routes/users.js:88
Bajo
Fix
// En producción, no exponer err.stack res.status(500).json({ error: 'Internal server error' });
#10
Paginación del export sin límite máximo — posible DoS
src/routes/leads.js:78
Bajo
Problema
El parámetro limit no tiene cota superior. Un cliente puede pedir 1.000.000 de leads en una sola request.
Fix
const limit = Math.min( parseInt(req.query.limit) || 100, 1000 // máximo absoluto );
Fase 3 — Checklist OWASP Top 10
  • A01 Broken Access Control — IDOR en /leads/export (#03)
  • A02 Cryptographic Failures — JWT secret hardcoded (#02) + MD5 passwords (#05)
  • A03 Injection — SQL Injection en login (#01)
  • A04 Insecure Design — Sin issues arquitectónicos graves
  • A05 Security Misconfiguration — CORS wildcard (#08, medio)
  • A06 Vulnerable Components — Dependencias no revisadas en este diff
  • A07 Auth Failures — Sin rate limiting (#04) + JWT sin expiración (#07)
  • A08 Software/Data Integrity — Sin verificación de integridad en imports
  • A09 Security Logging — Password en logs (#06)
  • A10 SSRF — Sin llamadas externas con input de usuario
🛠 Plan de Acción Recomendado
Prioridad Acción Archivo Esfuerzo est.
P0 Pasar a queries parametrizadas src/db/queries.js 2h
P0 Mover JWT_SECRET a .env + validar al arrancar src/utils/crypto.js 30min
P1 Añadir verificación de ownership en export src/routes/leads.js 1h
P1 Integrar express-rate-limit en login src/auth/login.js 1h
P1 Migrar hashPassword a bcrypt (cost 12) src/utils/crypto.js 2h + migración BD
P2 Eliminar password de logs, añadir expiresIn JWT, acotar CORS Varios 1h