codigo0/docs/AUDITORIA_SEGURIDAD_DEVOPS.md

1142 lines
30 KiB
Markdown

# 🔒 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`
```javascript
// ❌ VULNERABILIDAD CRÍTICA
const JWT_SECRET = process.env.JWT_SECRET || 'emerges-tes-secret-key-change-in-production';
```
**Problema:**
- Si `JWT_SECRET` no 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:**
```javascript
// ✅ 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`
```bash
# ❌ 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:**
```bash
# ✅ 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`
```javascript
// ⚠️ 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`, `status` no son validados contra valores permitidos
- Possible injection de formato (ej: `%' OR '1'='1` en 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:**
```javascript
// ✅ 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`
```typescript
// ❌ 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`
```typescript
// ❌ VULNERABILIDAD XSS
parent.innerHTML = `
<div class="flex flex-col items-center justify-center p-4 text-center">
<!-- ... -->
</div>
`;
```
**Problema:**
- `innerHTML` usado con contenido que puede contener código malicioso
- `error.message` puede contener payloads XSS
- Sin sanitización de HTML
**Impacto:** MEDIA-ALTA
- XSS si `error.message` contiene código malicioso
- Aunque improbable, puede ser explotado si hay logging de errores desde inputs del usuario
**Fix Inmediato:**
```typescript
// ✅ 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`
```typescript
// ⚠️ 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, `id` puede ser controlado externamente
- Si `id` contiene caracteres especiales, puede romper el CSS o causar problemas
**Impacto:** BAJA-MEDIA
- Menor riesgo porque es CSS, pero `id` debería ser validado
**Fix Inmediato:**
```typescript
// ✅ 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`
```javascript
// ❌ 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_ENV` en producción, toda la API está expuesta
**Impacto:** ALTA (solo en desarrollo, pero peligroso si se olvida)
**Fix Inmediato:**
```javascript
// ✅ 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:**
```javascript
// ✅ 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-By` expuesto (información del servidor)
**Impacto:** ALTA
- Vulnerable a XSS, Clickjacking, MIME sniffing
- Información del servidor expuesta
**Fix Inmediato:**
```javascript
// ✅ 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`
```javascript
// ✅ 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:**
```javascript
// ✅ 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:**
```javascript
// ✅ 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`
```javascript
// ✅ 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):**
```javascript
// ✅ 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`
```bash
# ⚠️ PROBLEMA: No verifica NODE_ENV antes de build
if npm run build; then
echo -e "${GREEN}✅ Build completado${NC}"
```
**Problema:**
- No fuerza `NODE_ENV=production` en build
- Puede construir con modo desarrollo
**Fix Sugerido:**
```bash
# ✅ 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`
```bash
# ❌ 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:**
```yaml
# ✅ 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`
```javascript
// ✅ 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.log` y `console.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:**
```javascript
// ✅ 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):**
```javascript
// ✅ 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 audit` en CI/CD
- No hay verificación automática de vulnerabilidades
- Dependencias no actualizadas regularmente
**Fix Inmediato:**
```bash
# ✅ FIX: Ejecutar npm audit
cd backend && npm audit
cd ../frontend && npm audit
cd ../admin-panel && npm audit
```
**Comandos Recomendados:**
```bash
# 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:**
```bash
# Verificar vulnerabilidades específicas
npm audit express
# Actualizar si hay patches
npm update express
```
---
### 4.3 Lock Files
#### ✅ Presente
**Estado:** ✅ Correcto
- `package-lock.json` presente en backend ✅
- `package-lock.json` presente en frontend ✅
- `package-lock.json` presente en admin-panel ✅
**Recomendación:**
- Asegurar que lock files están en `.gitignore` para `node_modules/` pero NO para `package-lock.json`
- Committear `package-lock.json` al 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)
1.**S1:** Validar JWT_SECRET (30 min)
2.**S7:** Rate Limiting (1 hora)
3.**S8:** Helmet.js (30 min)
4.**S6:** Fix CORS (30 min)
5.**S5:** Sanitizar chart.tsx (30 min)
6.**S4:** Fix XSS innerHTML (1 hora)
### Día 2-3: Validación y Webhook (9 horas)
7.**S3:** Validación Zod (8 horas) ⚠️ **GRANDE**
8.**S2:** Webhook HMAC (1 hora)
### Día 4: Infraestructura (2 horas)
9.**S9:** Validar env vars (1 hora)
10.**S12:** Fix deploy script (5 min)
11.**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)