security: comprehensive security audit and remediation (20 fixes)

CRITICAL fixes:
- Restrict X-View-Tenant impersonation to global admin only (was any admin)
- Add authorization to subscription endpoints (was open to any user)
- Make webhook signature verification mandatory (was skippable)
- Remove databaseName from JWT payload (resolve server-side with cache)
- Reduce body size limit from 1GB to 10MB (50MB for bulk CFDI)
- Restrict .env file permissions to 600

HIGH fixes:
- Add authorization to SAT cron endpoints (global admin only)
- Add Content-Security-Policy and Permissions-Policy headers
- Centralize isGlobalAdmin() utility with caching
- Add rate limiting on auth endpoints (express-rate-limit)
- Require authentication on logout endpoint

MEDIUM fixes:
- Replace Math.random() with crypto.randomBytes for temp passwords
- Remove console.log of temporary passwords in production
- Remove DB credentials from admin notification email
- Add escapeHtml() to email templates (prevent HTML injection)
- Add file size validation on FIEL upload (50KB max)
- Require TLS for SMTP connections
- Normalize email to lowercase before uniqueness check
- Remove hardcoded default for FIEL_ENCRYPTION_KEY

Also includes:
- Complete production deployment documentation
- API reference documentation
- Security audit report with remediation details
- Updated README with v0.5.0 changelog
- New client admin email template
- Utility scripts (create-carlos, test-emails)
- PM2 ecosystem config updates

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Consultoria AS
2026-03-18 22:32:04 +00:00
parent 38626bd3e6
commit 351b14a78c
31 changed files with 1287 additions and 103 deletions

View File

@@ -0,0 +1,143 @@
# 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