# 🔒 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 = `
Por favor, recarga la página. Si el problema persiste, limpia la caché del navegador.
Error: ${error instanceof Error ? error.message : String(error)}
Por favor, recarga la página. Si el problema persiste, limpia la caché del navegador.
Error: ${sanitizedError}
Por favor, recarga la página.
`; 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