# Auditoría de Seguridad y Remediación - Horux360 **Fecha:** 2026-03-18 **Auditor:** Claude Opus 4.6 **Alcance:** Plataforma completa (API, Frontend, Infraestructura) --- ## Resumen Ejecutivo Se realizó una auditoría de seguridad completa de la plataforma Horux360 antes de abrirla a clientes. Se identificaron **6 vulnerabilidades críticas, 9 altas, 10 medias y 7 bajas**. Se corrigieron **20 vulnerabilidades** (todas las críticas, altas y medias de código). ## Vulnerabilidades Corregidas ### CRÍTICAS (6) #### C1. Impersonación de Tenant sin Restricción - **Archivo:** `tenant.middleware.ts`, `plan-limits.middleware.ts` - **Problema:** Cualquier usuario con `role === 'admin'` (incluidos los admins de clientes) podía usar el header `X-View-Tenant` para acceder a los datos de CUALQUIER otro tenant. - **Fix:** Se creó `utils/global-admin.ts` con función `isGlobalAdmin()` que verifica que el tenant del usuario solicitante tenga el RFC del admin global (`CAS2408138W2`). Se aplicó en `tenant.middleware.ts` y `plan-limits.middleware.ts`. - **Impacto:** Rompía completamente el aislamiento multi-tenant. #### C2. Endpoints de Suscripción sin Autorización (IDOR) - **Archivo:** `subscription.routes.ts`, `subscription.controller.ts` - **Problema:** Cualquier usuario autenticado podía llamar `POST /api/subscriptions/:tenantId/mark-paid` para marcar cualquier tenant como pagado. - **Fix:** Se agregó `authorize('admin')` en las rutas y verificación `isGlobalAdmin()` en cada método del controlador. Doble capa de protección. - **Impacto:** Bypass total de pagos. #### C3. Bypass de Verificación de Webhook de MercadoPago - **Archivo:** `webhook.controller.ts`, `mercadopago.service.ts` - **Problema:** (1) Si faltaba el header `x-signature`, la verificación se saltaba completamente. (2) Si `MP_WEBHOOK_SECRET` no estaba configurado, la función retornaba `true` siempre. - **Fix:** Ahora es obligatorio que los headers `x-signature`, `x-request-id` y `data.id` estén presentes; de lo contrario se rechaza con 401. Si `MP_WEBHOOK_SECRET` no está configurado, se rechaza el webhook. - **Impacto:** Un atacante podía forjar webhooks para activar suscripciones gratis. #### C4. `databaseName` Expuesto en JWT - **Archivo:** `auth.service.ts`, `packages/shared/src/types/auth.ts`, `tenant.middleware.ts` - **Problema:** El nombre interno de la base de datos PostgreSQL se incluía en el JWT (base64, visible para cualquier usuario). - **Fix:** Se eliminó `databaseName` del payload JWT y del tipo `JWTPayload`. El tenant middleware ahora resuelve el `databaseName` server-side usando `tenantId` con caché de 5 minutos. - **Impacto:** Fuga de información de infraestructura interna. #### C5. Body Size Limit de 1GB - **Archivo:** `app.ts`, `cfdi.routes.ts`, `deploy/nginx/horux360.conf` - **Problema:** Express y Nginx aceptaban payloads de hasta 1GB, permitiendo DoS por agotamiento de memoria. - **Fix:** Límite global reducido a `10mb`. Ruta `/api/cfdi/bulk` tiene límite específico de `50mb`. Nginx actualizado a `50M`. - **Impacto:** Un solo request malicioso podía crashear el servidor. #### C6. Archivo `.env` con Permisos 644 - **Archivo:** `apps/api/.env` - **Problema:** El archivo `.env` era legible por cualquier usuario del sistema. - **Fix:** `chmod 600` — solo legible por el propietario (root). ### ALTAS (5) #### H1. SAT Cron Endpoints sin Autorización - **Archivo:** `sat.routes.ts`, `sat.controller.ts` - **Problema:** Cualquier usuario autenticado podía ejecutar el cron global de sincronización SAT. - **Fix:** Se agregó `authorize('admin')` en rutas y `isGlobalAdmin()` en el controlador. #### H2. Sin Content Security Policy (CSP) - **Archivo:** `deploy/nginx/horux360.conf` - **Problema:** Sin CSP, no había protección del navegador contra XSS. - **Fix:** Se agregó CSP header completo. Se removió `X-XSS-Protection` (deprecado). Se agregó `Permissions-Policy`. #### H3. Tenant CRUD con Admin Genérico - **Archivo:** `usuarios.controller.ts` - **Problema:** El check `isGlobalAdmin()` estaba duplicado y no centralizado. - **Fix:** Se centralizó en `utils/global-admin.ts` con caché para evitar queries repetidos. #### H4. Sin Rate Limiting en Auth - **Archivo:** `auth.routes.ts` - **Problema:** Sin límite de intentos en login/register/refresh. - **Fix:** `express-rate-limit` instalado con: login 10/15min, register 3/hora, refresh 20/15min por IP. #### H5. Logout Público - **Archivo:** `auth.routes.ts` - **Problema:** El endpoint `/auth/logout` no requería autenticación. - **Fix:** Se agregó `authenticate` middleware. ### MEDIAS (9) | # | Problema | Fix | |---|---------|-----| | M1 | Contraseñas temporales con `Math.random()` | Cambiado a `crypto.randomBytes(4).toString('hex')` | | M2 | Contraseñas temporales logueadas a console | Removido `console.log` | | M3 | Credenciales de BD enviadas por email | Removida sección de conexión DB del template de email | | M4 | HTML injection en templates de email | Agregado `escapeHtml()` en todos los valores interpolados | | M5 | Sin validación de tamaño en upload de FIEL | Límite de 50KB por archivo, 256 chars para password | | M6 | SMTP sin requerir TLS | Agregado `requireTLS: true` en config de Nodemailer | | M7 | Email no normalizado en registro | `toLowerCase()` aplicado antes del check de duplicados | | M8 | FIEL_ENCRYPTION_KEY con default hardcoded | Removido `.default()`, ahora es requerido | | M9 | Plan limits bypass con X-View-Tenant | Mismo fix que C1, verificación `isGlobalAdmin()` | ## Vulnerabilidades Pendientes (Infraestructura) Estas requieren cambios de infraestructura que no son código: | # | Severidad | Problema | Recomendación | |---|-----------|---------|---------------| | P1 | ALTA | App corre como root | Crear usuario `horux` dedicado | | P2 | MEDIA | PostgreSQL usa superuser | Crear usuario `horux_app` con permisos mínimos | | P3 | MEDIA | Backups sin encriptar ni offsite | Agregar GPG + sync a S3 | | P4 | MEDIA | Sin lockout de cuenta | Agregar contador de intentos fallidos (requiere migración DB) | | P5 | BAJA | Tokens JWT en localStorage | Migrar a HttpOnly cookies (requiere cambios frontend + API) | | P6 | BAJA | Mismo JWT secret para access y refresh | Agregar `JWT_REFRESH_SECRET` | ## Archivos Modificados ### Nuevos - `apps/api/src/utils/global-admin.ts` — Utilidad centralizada para verificar admin global con caché ### Modificados (Seguridad) - `apps/api/src/middlewares/tenant.middleware.ts` — Resolución de databaseName server-side + global admin check - `apps/api/src/middlewares/plan-limits.middleware.ts` — Global admin check para bypass - `apps/api/src/controllers/subscription.controller.ts` — Global admin authorization - `apps/api/src/controllers/webhook.controller.ts` — Verificación de firma obligatoria - `apps/api/src/controllers/sat.controller.ts` — Global admin check en cron endpoints - `apps/api/src/controllers/usuarios.controller.ts` — Uso de utilidad centralizada - `apps/api/src/controllers/fiel.controller.ts` — Validación de tamaño de archivos - `apps/api/src/routes/auth.routes.ts` — Rate limiting + logout autenticado - `apps/api/src/routes/subscription.routes.ts` — authorize('admin') middleware - `apps/api/src/routes/sat.routes.ts` — authorize('admin') en cron endpoints - `apps/api/src/routes/cfdi.routes.ts` — Límite de 50MB específico para bulk - `apps/api/src/services/auth.service.ts` — databaseName removido de JWT, email normalizado - `apps/api/src/services/usuarios.service.ts` — randomBytes + sin console.log - `apps/api/src/services/email/email.service.ts` — requireTLS - `apps/api/src/services/email/templates/new-client-admin.ts` — Sin DB credentials, con escapeHtml - `apps/api/src/services/payment/mercadopago.service.ts` — Rechazar si no hay secret - `apps/api/src/config/env.ts` — FIEL_ENCRYPTION_KEY requerido - `apps/api/src/app.ts` — Body limit 10MB - `packages/shared/src/types/auth.ts` — databaseName removido de JWTPayload - `deploy/nginx/horux360.conf` — CSP, Permissions-Policy, body 50M ## Prácticas Positivas Encontradas - bcrypt con 12 salt rounds - HTTPS con HSTS, TLS 1.2/1.3 - Helmet.js activo - SQL parameterizado en todas las queries raw (Prisma ORM) - FIEL encriptado con AES-256-GCM - Refresh token rotation implementada - Base de datos por tenant (aislamiento a nivel DB) - PostgreSQL solo escucha en localhost - `.env` en `.gitignore` y nunca commiteado