# Auditoría de Seguridad y Remediación - Horux360

**Fecha:** 2026-03-18
**Auditor:** Carlos e Ivan (Horux360)
**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 (`HTS240708LJA`). 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
