codigo0/docs/AUDITORIA_SEGURIDAD_DEVOPS.md

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

// ✅ 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, 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:

// ✅ 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:

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

// ✅ 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, 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:

// ✅ 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_ENV en 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-By expuesto (información del servidor)

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=production en 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.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:

// ✅ 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 audit en 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.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
S2 Webhook Secret hardcoded webhook-deploy.sh 10 1 hora
S3 Sin validación de inputs backend/src/routes/*.js Múltiples 8 horas
S4 XSS en innerHTML src/main.tsx 137 1 hora
S5 dangerouslySetInnerHTML sin sanitización src/components/ui/chart.tsx 70 30 min
S6 CORS permisivo en dev backend/src/index.js 37-53 30 min
S7 Sin rate limiting backend/src/index.js - 1 hora
S8 Sin headers de seguridad (Helmet) backend/src/index.js - 30 min

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

  1. S3: Validación Zod (8 horas) ⚠️ GRANDE
  2. S2: Webhook HMAC (1 hora)

Día 4: Infraestructura (2 horas)

  1. S9: Validar env vars (1 hora)
  2. S12: Fix deploy script (5 min)
  3. 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)