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
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
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
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
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
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
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
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
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
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
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 |