30 KiB
🔒 AUDITORÍA DE SEGURIDAD Y DEVOPS
Fecha: 2025-01-07
Auditor: Especialista en Seguridad y DevOps
Versión del Proyecto: 1.0.0
📊 RESUMEN EJECUTIVO
| Categoría | Estado | Calificación | Críticas |
|---|---|---|---|
| Vulnerabilidades Inmediatas | 🔴 CRÍTICO | 4/10 | 6 |
| Buenas Prácticas | ⚠️ BÁSICA | 5.5/10 | - |
| Infraestructura/Deploy | ⚠️ MEJORABLE | 6/10 | 2 |
| Dependencias | ⚠️ NO VERIFICADO | 5/10 | - |
Calificación General: 5.1/10 🔴 VULNERABLE - ACCIÓN INMEDIATA REQUERIDA
🔴 1. VULNERABILIDADES CRÍTICAS INMEDIATAS
1.1 Hardcoded Secrets
🔴 CRÍTICA #1: JWT Secret con Fallback Débil
Ubicación: backend/src/routes/auth.js:11 y backend/src/middleware/auth.js:8
// ❌ VULNERABILIDAD CRÍTICA
const JWT_SECRET = process.env.JWT_SECRET || 'emerges-tes-secret-key-change-in-production';
Problema:
- Si
JWT_SECRETno está en.env, usa un secret débil conocido - Secret por defecto es predecible y está hardcodeado en el código
- Cualquier atacante puede forjar tokens JWT si no hay
.env
Impacto: CRÍTICO
- Autenticación comprometida completamente
- Cualquiera puede generar tokens válidos
- Acceso no autorizado a toda la API
Fix Inmediato:
// ✅ FIX: Validar que JWT_SECRET existe en startup
import dotenv from 'dotenv';
dotenv.config();
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET === 'emerges-tes-secret-key-change-in-production') {
console.error('❌ CRÍTICO: JWT_SECRET no está configurado o usa valor por defecto');
console.error(' Configura JWT_SECRET en .env (generar con: openssl rand -base64 32)');
process.exit(1);
}
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '24h';
Archivos a modificar:
backend/src/routes/auth.js(línea 11)backend/src/middleware/auth.js(línea 8)backend/src/index.js(añadir validación en startup)
Esfuerzo: 30 minutos
Prioridad: 🔴 1º (CRÍTICA - HACER YA)
🔴 CRÍTICA #2: Webhook Secret Hardcoded
Ubicación: webhook-deploy.sh:10
# ❌ VULNERABILIDAD CRÍTICA
SECRET="TU_SECRET_AQUI" # Cambiar por un secret seguro
Problema:
- Secret hardcoded en script de deploy
- Script puede ser ejecutado por cualquiera que tenga acceso al repositorio
- Sin verificación HMAC de webhook de GitHub
Impacto: ALTA
- Deploy automático sin autenticación
- Posible compromiso del servidor de producción
Fix Inmediato:
# ✅ FIX: Usar variable de entorno
SECRET="${WEBHOOK_SECRET}" # Configurar en .env o variables del sistema
if [ -z "$SECRET" ]; then
log "ERROR: WEBHOOK_SECRET no está configurado"
exit 1
fi
# Verificar HMAC de GitHub
SIGNATURE=$(echo "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
GITHUB_SIG=$(echo "$PAYLOAD" | jq -r '.headers["X-Hub-Signature-256"]' | cut -d'=' -f2)
if [ "$SIGNATURE" != "$GITHUB_SIG" ]; then
log "ERROR: Invalid webhook signature"
exit 1
fi
Archivo a modificar:
webhook-deploy.sh(línea 10, añadir verificación HMAC)
Esfuerzo: 1 hora
Prioridad: 🔴 2º (CRÍTICA)
1.2 Inyecciones Potenciales
🔴 CRÍTICA #3: Sin Validación de Inputs (SQL Injection Mitigado pero Falta Validación)
Ubicación: backend/src/routes/content.js:19-114
// ⚠️ PROBLEMA: Usa parámetros ($1, $2) pero NO valida formato de inputs
const { type, level, status, category, page = 1, pageSize = 20, search } = req.query;
// Problema: search puede contener caracteres especiales
if (search) {
whereConditions.push(`(title ILIKE $${paramIndex} OR short_title ILIKE $${paramIndex})`);
params.push(`%${search}%`); // ⚠️ Sin validación de formato
paramIndex++;
}
Problema:
- SQL Injection mitigado con parámetros ($1, $2), pero falta validación de formato
- Inputs como
search,type,statusno son validados contra valores permitidos - Possible injection de formato (ej:
%' OR '1'='1en search)
Impacto: ALTA
- Aunque protegido contra SQL injection clásico, falta validación puede causar comportamientos inesperados
- Error en queries si valores no son del formato esperado
Fix Inmediato:
// ✅ FIX: Añadir validación con Zod
import { z } from 'zod';
const contentQuerySchema = z.object({
type: z.enum(['protocol', 'guide', 'drug', 'checklist', 'manual']).optional(),
level: z.enum(['operativo', 'formativo']).optional(),
status: z.enum(['draft', 'in_review', 'approved', 'published', 'archived']).optional(),
category: z.string().max(100).optional(),
page: z.coerce.number().int().positive().default(1),
pageSize: z.coerce.number().int().positive().max(100).default(20),
search: z.string().max(200).optional(),
});
router.get('/', requirePermission('content:read'), async (req, res) => {
try {
// Validar y sanitizar inputs
const validated = contentQuerySchema.parse(req.query);
const { type, level, status, category, page, pageSize, search } = validated;
// Sanitizar search: remover caracteres especiales peligrosos
const sanitizedSearch = search
? search.replace(/[%_'"]/g, '').trim() // Remover caracteres SQL especiales
: undefined;
// ... resto de la lógica
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ error: 'Parámetros inválidos', details: error.errors });
}
throw error;
}
});
Archivos a modificar:
backend/src/routes/content.js(añadir validación en todas las rutas)backend/src/routes/drugs.js(añadir validación)backend/src/routes/media.js(añadir validación)backend/src/routes/auth.js(añadir validación de email)
Esfuerzo: 2 horas por archivo (8 horas total)
Prioridad: 🔴 3º (ALTA)
⚠️ CRÍTICA #4: XSS en Frontend (innerHTML sin Sanitización)
Ubicación 1: src/main.tsx:137
// ❌ VULNERABILIDAD XSS
rootElement.innerHTML = `
<div style="padding: 2rem; text-align: center; font-family: sans-serif;">
<h1>Error al cargar la aplicación</h1>
<p>Por favor, recarga la página. Si el problema persiste, limpia la caché del navegador.</p>
<p style="color: #666; font-size: 0.9rem;">Error: ${error instanceof Error ? error.message : String(error)}</p>
</div>
`;
Ubicación 2: src/pages/GaleriaImagenes.tsx:159
// ❌ VULNERABILIDAD XSS
parent.innerHTML = `
<div class="flex flex-col items-center justify-center p-4 text-center">
<!-- ... -->
</div>
`;
Problema:
innerHTMLusado con contenido que puede contener código maliciosoerror.messagepuede contener payloads XSS- Sin sanitización de HTML
Impacto: MEDIA-ALTA
- XSS si
error.messagecontiene código malicioso - Aunque improbable, puede ser explotado si hay logging de errores desde inputs del usuario
Fix Inmediato:
// ✅ FIX: Sanitizar o usar DOM seguro
import DOMPurify from 'isomorphic-dompurify';
// Opción 1: Sanitizar con DOMPurify
const sanitizedError = DOMPurify.sanitize(error instanceof Error ? error.message : String(error));
rootElement.innerHTML = `
<div style="padding: 2rem; text-align: center; font-family: sans-serif;">
<h1>Error al cargar la aplicación</h1>
<p>Por favor, recarga la página. Si el problema persiste, limpia la caché del navegador.</p>
<p style="color: #666; font-size: 0.9rem;">Error: ${sanitizedError}</p>
</div>
`;
// Opción 2 (MEJOR): Usar DOM seguro (recomendado)
const errorDiv = document.createElement('div');
errorDiv.className = 'error-container';
errorDiv.innerHTML = `
<h1>Error al cargar la aplicación</h1>
<p>Por favor, recarga la página.</p>
`;
const errorText = document.createElement('p');
errorText.style.color = '#666';
errorText.style.fontSize = '0.9rem';
errorText.textContent = `Error: ${error instanceof Error ? error.message : String(error)}`;
errorDiv.appendChild(errorText);
rootElement.innerHTML = '';
rootElement.appendChild(errorDiv);
Archivos a modificar:
src/main.tsx(línea 137)src/pages/GaleriaImagenes.tsx(línea 159)
Esfuerzo: 1 hora
Prioridad: 🟡 4º (MEDIA-ALTA)
⚠️ CRÍTICA #5: dangerouslySetInnerHTML sin Sanitización
Ubicación: src/components/ui/chart.tsx:70
// ⚠️ PROBLEMA: dangerouslySetInnerHTML con contenido dinámico
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(([theme, prefix]) => `
${prefix} [data-chart=${id}] {
/* ... CSS dinámico ... */
}`)
.join('\n')
}}
/>
Problema:
- Aunque CSS es menos peligroso que HTML,
idpuede ser controlado externamente - Si
idcontiene caracteres especiales, puede romper el CSS o causar problemas
Impacto: BAJA-MEDIA
- Menor riesgo porque es CSS, pero
iddebería ser validado
Fix Inmediato:
// ✅ FIX: Validar y sanitizar id
const sanitizeId = (id: string): string => {
// Solo permitir caracteres alfanuméricos, guiones y guiones bajos
return id.replace(/[^a-zA-Z0-9_-]/g, '');
};
// En el componente:
const safeId = sanitizeId(id);
<style
dangerouslySetInnerHTML={{
__html: Object.entries(THEMES)
.map(([theme, prefix]) => `
${prefix} [data-chart="${safeId}"] {
/* ... CSS dinámico ... */
}`)
.join('\n')
}}
/>
Archivo a modificar:
src/components/ui/chart.tsx(línea 70)
Esfuerzo: 30 minutos
Prioridad: 🟡 5º (BAJA-MEDIA)
1.3 Autenticación/Autorización
🔴 CRÍTICA #6: CORS Permisivo en Desarrollo
Ubicación: backend/src/index.js:37-53
// ❌ VULNERABILIDAD: CORS acepta cualquier origen en desarrollo
app.use(cors({
origin: (origin, callback) => {
// Permitir requests sin origen (mobile apps, Postman, etc.) en desarrollo
if (!origin || process.env.NODE_ENV === 'development') {
return callback(null, true); // ⚠️ Acepta CUALQUIER origen en dev
}
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // ⚠️ Con credentials: true, esto es peligroso
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
Problema:
- En desarrollo, acepta CUALQUIER origen (incluso malicioso)
- Con
credentials: true, esto permite que cualquier sitio web pueda hacer requests autenticados - Si se olvida cambiar
NODE_ENVen producción, toda la API está expuesta
Impacto: ALTA (solo en desarrollo, pero peligroso si se olvida)
Fix Inmediato:
// ✅ FIX: Limitar orígenes incluso en desarrollo
const isDevelopment = process.env.NODE_ENV === 'development';
const defaultDevOrigins = [
'http://localhost:8096',
'http://localhost:5174',
'http://localhost:5173',
'http://127.0.0.1:8096',
'http://127.0.0.1:5174',
'http://127.0.0.1:5173',
];
const allowedOrigins = process.env.CORS_ORIGINS
? process.env.CORS_ORIGINS.split(',').map(origin => origin.trim())
: (isDevelopment ? defaultDevOrigins : []); // En producción, requerir CORS_ORIGINS
app.use(cors({
origin: (origin, callback) => {
// Permitir requests sin origen SOLO para mobile apps o herramientas CLI (con verificación)
if (!origin) {
// En producción, rechazar requests sin origen (excepto health check)
if (!isDevelopment && req.path !== '/health') {
return callback(new Error('Origin required in production'));
}
// En desarrollo, permitir para testing local
return callback(null, true);
}
if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
console.warn(`[CORS] Origin bloqueado: ${origin}`);
callback(new Error('Not allowed by CORS'));
}
},
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization'],
maxAge: 86400, // Cache preflight por 24 horas
}));
Archivo a modificar:
backend/src/index.js(línea 37-53)
Esfuerzo: 30 minutos
Prioridad: 🔴 6º (ALTA)
🔴 CRÍTICA #7: Sin Rate Limiting
Ubicación: backend/src/index.js (no existe)
Problema:
- ❌ NO HAY RATE LIMITING
- Cualquier atacante puede hacer requests ilimitados
- Vulnerable a:
- Brute Force Attacks (login)
- DDoS Attacks
- API Abuse
- Resource Exhaustion
Impacto: CRÍTICO
Fix Inmediato:
// ✅ FIX: Instalar express-rate-limit
// npm install express-rate-limit
import rateLimit from 'express-rate-limit';
// Rate limiter general para todas las rutas
const generalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 100, // 100 requests por IP por ventana
message: 'Demasiadas requests desde esta IP, por favor intenta de nuevo más tarde',
standardHeaders: true,
legacyHeaders: false,
skip: (req) => req.path === '/health', // Skip health checks
});
// Rate limiter estricto para login
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutos
max: 5, // 5 intentos de login por IP por ventana
message: 'Demasiados intentos de login, por favor intenta de nuevo en 15 minutos',
skipSuccessfulRequests: true, // No contar requests exitosos
});
// Rate limiter para creación de contenido
const contentWriteLimiter = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora
max: 50, // 50 creaciones/actualizaciones por IP por hora
message: 'Límite de operaciones de escritura alcanzado',
});
app.use(generalLimiter);
app.use('/api/auth/login', authLimiter);
app.use('/api/auth/register', authLimiter);
app.use('/api/content', contentWriteLimiter); // Solo para POST/PUT/DELETE
Archivo a modificar:
backend/src/index.js(añadir antes de routes)backend/package.json(añadir dependencia)
Esfuerzo: 1 hora
Prioridad: 🔴 7º (CRÍTICA)
1.4 Headers de Seguridad HTTP
🔴 CRÍTICA #8: Sin Headers de Seguridad (Helmet.js)
Ubicación: backend/src/index.js (no existe)
Problema:
- ❌ NO HAY HELMET.JS
- Sin headers de seguridad HTTP:
- Sin
Content-Security-Policy(XSS protection) - Sin
X-Frame-Options(Clickjacking protection) - Sin
X-Content-Type-Options(MIME sniffing protection) - Sin
Strict-Transport-Security(HSTS) - Sin
X-XSS-Protection - Sin
Referrer-Policy X-Powered-Byexpuesto (información del servidor)
- Sin
Impacto: ALTA
- Vulnerable a XSS, Clickjacking, MIME sniffing
- Información del servidor expuesta
Fix Inmediato:
// ✅ FIX: Instalar y configurar Helmet.js
// npm install helmet
import helmet from 'helmet';
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"], // Necesario para algunos componentes
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
mediaSrc: ["'self'"],
frameSrc: ["'none'"],
},
},
crossOriginEmbedderPolicy: false, // Deshabilitar si causa problemas con CORS
crossOriginResourcePolicy: { policy: "cross-origin" }, // Permitir recursos cross-origin
hsts: {
maxAge: 31536000, // 1 año
includeSubDomains: true,
preload: true,
},
}));
// Remover X-Powered-By manualmente (Helmet lo hace automáticamente)
app.disable('x-powered-by');
Archivo a modificar:
backend/src/index.js(añadir después de cors, antes de routes)backend/package.json(añadir dependencia)
Esfuerzo: 30 minutos
Prioridad: 🔴 8º (ALTA)
2. ✅ BUENAS PRÁCTICAS
2.1 Variables de Entorno
✅ Correcto
Ubicación: backend/config/database.js, backend/src/routes/auth.js
// ✅ BUENO: Usa variables de entorno
const pool = new Pool({
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '5432', 10),
database: process.env.DB_NAME || 'emerges_tes',
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || '',
});
Estado: ✅ Correcto (excepto JWT_SECRET que tiene fallback débil)
⚠️ Mejorable
Ubicación: backend/ENV_TEMPLATE.md
Problema:
- Template existe, pero no está en
.env.example(estándar de la industria) - No hay validación de variables requeridas en startup
Fix Sugerido:
// ✅ FIX: Validar variables de entorno en startup
// backend/src/utils/env-validator.js
import dotenv from 'dotenv';
dotenv.config();
const requiredEnvVars = [
'DB_HOST',
'DB_PORT',
'DB_NAME',
'DB_USER',
'DB_PASSWORD',
'JWT_SECRET',
];
const optionalEnvVars = {
'DB_PORT': '5432',
'API_PORT': '3000',
'JWT_EXPIRES_IN': '24h',
'NODE_ENV': 'development',
};
export function validateEnv() {
const missing = requiredEnvVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
console.error('❌ CRÍTICO: Variables de entorno faltantes:');
missing.forEach(varName => {
console.error(` - ${varName}`);
});
console.error('\n Configura estas variables en .env (ver backend/ENV_TEMPLATE.md)');
process.exit(1);
}
// Validar formato de JWT_SECRET
if (process.env.JWT_SECRET && process.env.JWT_SECRET.length < 32) {
console.error('❌ CRÍTICO: JWT_SECRET debe tener al menos 32 caracteres');
console.error(' Generar con: openssl rand -base64 32');
process.exit(1);
}
// Validar formato de DB_PASSWORD
if (process.env.DB_PASSWORD && process.env.DB_PASSWORD.length < 8) {
console.warn('⚠️ ADVERTENCIA: DB_PASSWORD es muy corto (recomendado: mínimo 12 caracteres)');
}
console.log('✅ Variables de entorno validadas correctamente');
}
// En backend/src/index.js
import { validateEnv } from './utils/env-validator.js';
validateEnv();
Esfuerzo: 1 hora
Prioridad: 🟡 MEDIA
2.2 Validación de Inputs
⚠️ Estado Actual: Parcial
Problema:
- ❌ Sin validación con Zod o Joi
- ⚠️ Solo validaciones básicas (
if (!email || !password)) - ⚠️ Sin sanitización de strings
- ⚠️ Sin validación de tipos
Fix Sugerido:
// ✅ FIX: Crear middleware de validación
// backend/src/middleware/validator.js
import { z } from 'zod';
export const validate = (schema) => {
return (req, res, next) => {
try {
req.body = schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
error: 'Validation failed',
details: error.errors,
});
}
next(error);
}
};
};
// Esquemas de validación
export const loginSchema = z.object({
email: z.string().email().max(255).toLowerCase().trim(),
password: z.string().min(8).max(100),
});
export const createContentSchema = z.object({
id: z.string().regex(/^[a-z0-9-]+$/).min(3).max(100),
type: z.enum(['protocol', 'guide', 'drug', 'checklist', 'manual']),
title: z.string().min(3).max(200).trim(),
// ... más campos
});
Esfuerzo: 4 horas
Prioridad: 🔴 ALTA (ya incluido en CRÍTICA #3)
2.3 Manejo de Sesiones/Tokens
✅ Correcto
Ubicación: backend/src/routes/auth.js:59-63
// ✅ BUENO: JWT con expiración
const token = jwt.sign(
{ userId: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
Estado: ✅ Correcto
⚠️ Mejorable
Problema:
- Sin refresh tokens
- Sin blacklist de tokens revocados
- Sin rotación de secrets
Fix Sugerido (Opcional - Mejora futura):
// ✅ MEJORA FUTURA: Refresh tokens
// backend/src/routes/auth.js
router.post('/refresh', async (req, res) => {
const { refreshToken } = req.body;
// Verificar refresh token
// Generar nuevo access token
// Retornar nuevo token
});
Prioridad: 🟢 BAJA (no crítico ahora)
3. 🏗️ INFRAESTRUCTURA Y DEPLOY
3.1 Scripts de Deploy
⚠️ Problema 1: Deploy Script sin Validación de Entorno
Ubicación: deploy.sh:71
# ⚠️ PROBLEMA: No verifica NODE_ENV antes de build
if npm run build; then
echo -e "${GREEN}✅ Build completado${NC}"
Problema:
- No fuerza
NODE_ENV=productionen build - Puede construir con modo desarrollo
Fix Sugerido:
# ✅ FIX: Forzar producción
echo -e "${YELLOW}🔨 [4/5] Construyendo aplicación (producción)...${NC}"
if NODE_ENV=production npm run build; then
echo -e "${GREEN}✅ Build completado${NC}"
Esfuerzo: 5 minutos
Prioridad: 🟡 MEDIA
⚠️ Problema 2: Webhook Script sin Verificación HMAC
Ubicación: webhook-deploy.sh:29-33
# ❌ PROBLEMA: No verifica HMAC de GitHub
if [ -n "$SECRET" ] && [ "$SECRET" != "TU_SECRET_AQUI" ]; then
SIGNATURE=$(echo "$PAYLOAD" | jq -r '.signature // empty')
# Aquí deberías verificar el HMAC, pero para simplicidad lo omitimos
# En producción, implementar verificación HMAC
fi
Problema:
- Comentario indica que falta verificación HMAC
- Cualquier request puede disparar deploy
Fix: Ya incluido en CRÍTICA #2
3.2 Configuración de Servidores
✅ Dockerfile: Bueno
Ubicación: Dockerfile
Estado: ✅ Correcto
- Multi-stage build ✅
- Health check ✅
- Usa usuario no-root (implicito con Alpine) ✅
- Variables de entorno ✅
⚠️ Docker Compose: Falta Seguridad
Ubicación: docker-compose.yml, docker-compose.prod.yml
Problemas:
- No hay límites de recursos explícitos (solo en prod)
- No hay usuario no-root especificado
- No hay read-only filesystem
- Sin secrets management (passwords en environment)
Fix Sugerido:
# ✅ FIX: docker-compose.prod.yml
services:
emerges-tes:
# ... existing config ...
user: "1000:1000" # Ejecutar como usuario no-root
read_only: true # Filesystem read-only
tmpfs:
- /tmp
- /var/cache
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # Solo necesario para binding a puertos <1024
secrets:
- db_password
- jwt_secret
secrets:
db_password:
external: true
jwt_secret:
external: true
Esfuerzo: 1 hora
Prioridad: 🟡 MEDIA
3.3 Health Checks, Logging, Monitoring
✅ Health Check Implementado
Ubicación: backend/src/index.js:59-66
// ✅ BUENO: Health check existe
app.get('/health', async (req, res) => {
const dbConnected = await testConnection();
res.json({
status: 'ok',
timestamp: new Date().toISOString(),
database: dbConnected ? 'connected' : 'disconnected'
});
});
Estado: ✅ Correcto
⚠️ Logging Inadecuado
Problema:
- Solo
console.logyconsole.error - Sin logging estructurado
- Sin niveles de log (INFO, WARN, ERROR)
- Sin rotación de logs
- Sin sanitización de datos sensibles en logs
Fix Sugerido:
// ✅ FIX: Implementar logging estructurado con Winston
// npm install winston
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'emerges-tes-backend' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple()
}));
}
// Sanitizar datos sensibles
logger.info('User login', {
userId: user.id,
email: user.email.replace(/(.{2})(.*)(@.*)/, '$1***$3'), // Ocultar email parcialmente
// NO loggear password_hash
});
Esfuerzo: 2 horas
Prioridad: 🟡 MEDIA
❌ Monitoring No Implementado
Problema:
- Sin métricas de rendimiento
- Sin alertas
- Sin APM (Application Performance Monitoring)
Fix Sugerido (Mejora Futura):
// ✅ MEJORA FUTURA: Añadir métricas con Prometheus
// npm install prom-client express-prometheus-middleware
import promClient from 'prom-client';
import promMiddleware from 'express-prometheus-middleware';
app.use(promMiddleware({
metricsPath: '/metrics',
collectDefaultMetrics: true,
}));
Prioridad: 🟢 BAJA (mejora futura)
4. 📦 DEPENDENCIAS
4.1 Análisis de Vulnerabilidades
⚠️ No Verificado
Problema:
- No hay
npm auditen CI/CD - No hay verificación automática de vulnerabilidades
- Dependencias no actualizadas regularmente
Fix Inmediato:
# ✅ FIX: Ejecutar npm audit
cd backend && npm audit
cd ../frontend && npm audit
cd ../admin-panel && npm audit
Comandos Recomendados:
# Verificar vulnerabilidades
npm audit
# Verificar vulnerabilidades con detalle
npm audit --audit-level=moderate
# Corregir vulnerabilidades automáticamente (si es posible)
npm audit fix
# Actualizar dependencias
npm update
# Verificar versiones desactualizadas
npm outdated
Esfuerzo: 30 minutos (verificación inicial)
Prioridad: 🔴 ALTA
4.2 Dependencias Vulnerables Conocidas
⚠️ Express 4.18.2
Problema:
- Express 4.18.2 tiene vulnerabilidades conocidas (verificar con
npm audit) - Debe actualizarse a última versión 4.x o considerar Express 5
Fix Sugerido:
# Verificar vulnerabilidades específicas
npm audit express
# Actualizar si hay patches
npm update express
4.3 Lock Files
✅ Presente
Estado: ✅ Correcto
package-lock.jsonpresente en backend ✅package-lock.jsonpresente en frontend ✅package-lock.jsonpresente en admin-panel ✅
Recomendación:
- Asegurar que lock files están en
.gitignoreparanode_modules/pero NO parapackage-lock.json - Committear
package-lock.jsonal repositorio ✅
5. 📋 RESUMEN DE VULNERABILIDADES
🔴 CRÍTICAS (8)
| ID | Vulnerabilidad | Archivo | Línea | Esfuerzo Fix | Prioridad |
|---|---|---|---|---|---|
| S1 | JWT Secret con fallback débil | backend/src/routes/auth.js |
11 | 30 min | 1º |
| S2 | Webhook Secret hardcoded | webhook-deploy.sh |
10 | 1 hora | 2º |
| S3 | Sin validación de inputs | backend/src/routes/*.js |
Múltiples | 8 horas | 3º |
| S4 | XSS en innerHTML | src/main.tsx |
137 | 1 hora | 4º |
| S5 | dangerouslySetInnerHTML sin sanitización | src/components/ui/chart.tsx |
70 | 30 min | 5º |
| S6 | CORS permisivo en dev | backend/src/index.js |
37-53 | 30 min | 6º |
| S7 | Sin rate limiting | backend/src/index.js |
- | 1 hora | 7º |
| S8 | Sin headers de seguridad (Helmet) | backend/src/index.js |
- | 30 min | 8º |
Total Críticas: 8
Esfuerzo Total: ~13 horas (1.5 días)
🟡 IMPORTANTES (6)
| ID | Vulnerabilidad | Archivo | Esfuerzo Fix | Prioridad |
|---|---|---|---|---|
| S9 | Validación de variables de entorno | backend/src/index.js |
1 hora | 9º |
| S10 | Logging inadecuado | backend/src/**/*.js |
2 horas | 10º |
| S11 | Docker Compose sin seguridad | docker-compose.prod.yml |
1 hora | 11º |
| S12 | Deploy script sin validación | deploy.sh |
5 min | 12º |
| S13 | Dependencias no auditadas | package.json |
30 min | 13º |
| S14 | Sin refresh tokens | backend/src/routes/auth.js |
4 horas | 14º (futuro) |
Total Importantes: 6
Esfuerzo Total: ~8.5 horas
🟢 MEJORAS (3)
| ID | Mejora | Esfuerzo | Prioridad |
|---|---|---|---|
| S15 | Monitoring con Prometheus | 1 día | Futuro |
| S16 | Rotación de secrets | 4 horas | Futuro |
| S17 | Blacklist de tokens | 2 horas | Futuro |
6. ✅ CHECKLIST DE IMPLEMENTACIÓN
Quick Wins (Día 1 - 4 horas)
- S1: Validar JWT_SECRET en startup (30 min)
- S7: Instalar y configurar express-rate-limit (1 hora)
- S8: Instalar y configurar Helmet.js (30 min)
- S6: Fix CORS permisivo (30 min)
- S5: Sanitizar id en chart.tsx (30 min)
- S4: Fix XSS en innerHTML (1 hora)
Total: ~4 horas
Refactors Importantes (Día 2-3 - 8 horas)
- S2: Fix webhook secret con HMAC (1 hora)
- S3: Implementar validación Zod en todas las rutas (8 horas) ⚠️ GRANDE
Total: ~9 horas
Mejoras de Infraestructura (Día 4 - 2 horas)
- S9: Validar variables de entorno (1 hora)
- S12: Fix deploy script (5 min)
- S13: Ejecutar npm audit y corregir (30 min)
Total: ~2 horas
7. 📊 TABLA COMPARATIVA PRIORIZADA
| Prioridad | ID | Vulnerabilidad | Severidad | Esfuerzo | Impacto | ROI |
|---|---|---|---|---|---|---|
| 1 | S1 | JWT Secret fallback | 🔴 CRÍTICA | 30 min | Alto | ⭐⭐⭐⭐⭐ |
| 2 | S7 | Sin rate limiting | 🔴 CRÍTICA | 1 hora | Alto | ⭐⭐⭐⭐⭐ |
| 3 | S8 | Sin Helmet.js | 🔴 CRÍTICA | 30 min | Alto | ⭐⭐⭐⭐⭐ |
| 4 | S6 | CORS permisivo | 🔴 CRÍTICA | 30 min | Medio | ⭐⭐⭐⭐ |
| 5 | S2 | Webhook secret | 🔴 CRÍTICA | 1 hora | Medio | ⭐⭐⭐⭐ |
| 6 | S3 | Sin validación inputs | 🔴 CRÍTICA | 8 horas | Alto | ⭐⭐⭐⭐ |
| 7 | S4 | XSS innerHTML | 🟡 ALTA | 1 hora | Medio | ⭐⭐⭐ |
| 8 | S5 | dangerouslySetInnerHTML | 🟡 ALTA | 30 min | Bajo | ⭐⭐⭐ |
| 9 | S9 | Validar env vars | 🟡 MEDIA | 1 hora | Medio | ⭐⭐⭐ |
| 10 | S10 | Logging estructurado | 🟡 MEDIA | 2 horas | Medio | ⭐⭐⭐ |
8. 🚨 PLAN DE ACCIÓN INMEDIATO
Día 1: Seguridad Crítica (4 horas)
- ✅ S1: Validar JWT_SECRET (30 min)
- ✅ S7: Rate Limiting (1 hora)
- ✅ S8: Helmet.js (30 min)
- ✅ S6: Fix CORS (30 min)
- ✅ S5: Sanitizar chart.tsx (30 min)
- ✅ S4: Fix XSS innerHTML (1 hora)
Día 2-3: Validación y Webhook (9 horas)
- ✅ S3: Validación Zod (8 horas) ⚠️ GRANDE
- ✅ S2: Webhook HMAC (1 hora)
Día 4: Infraestructura (2 horas)
- ✅ S9: Validar env vars (1 hora)
- ✅ S12: Fix deploy script (5 min)
- ✅ S13: npm audit (30 min)
Total: ~15 horas (2 días de trabajo)
Última actualización: 2025-01-07
Próxima revisión recomendada: Después de implementar fixes críticos (1 semana)