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>
8.3 KiB
8.3 KiB
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 headerX-View-Tenantpara acceder a los datos de CUALQUIER otro tenant. - Fix: Se creó
utils/global-admin.tscon funciónisGlobalAdmin()que verifica que el tenant del usuario solicitante tenga el RFC del admin global (CAS2408138W2). Se aplicó entenant.middleware.tsyplan-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-paidpara marcar cualquier tenant como pagado. - Fix: Se agregó
authorize('admin')en las rutas y verificaciónisGlobalAdmin()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) SiMP_WEBHOOK_SECRETno estaba configurado, la función retornabatruesiempre. - Fix: Ahora es obligatorio que los headers
x-signature,x-request-idydata.idestén presentes; de lo contrario se rechaza con 401. SiMP_WEBHOOK_SECRETno 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ó
databaseNamedel payload JWT y del tipoJWTPayload. El tenant middleware ahora resuelve eldatabaseNameserver-side usandotenantIdcon 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/bulktiene límite específico de50mb. Nginx actualizado a50M. - Impacto: Un solo request malicioso podía crashear el servidor.
C6. Archivo .env con Permisos 644
- Archivo:
apps/api/.env - Problema: El archivo
.envera 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 yisGlobalAdmin()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.tscon 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-limitinstalado con: login 10/15min, register 3/hora, refresh 20/15min por IP.
H5. Logout Público
- Archivo:
auth.routes.ts - Problema: El endpoint
/auth/logoutno requería autenticación. - Fix: Se agregó
authenticatemiddleware.
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 checkapps/api/src/middlewares/plan-limits.middleware.ts— Global admin check para bypassapps/api/src/controllers/subscription.controller.ts— Global admin authorizationapps/api/src/controllers/webhook.controller.ts— Verificación de firma obligatoriaapps/api/src/controllers/sat.controller.ts— Global admin check en cron endpointsapps/api/src/controllers/usuarios.controller.ts— Uso de utilidad centralizadaapps/api/src/controllers/fiel.controller.ts— Validación de tamaño de archivosapps/api/src/routes/auth.routes.ts— Rate limiting + logout autenticadoapps/api/src/routes/subscription.routes.ts— authorize('admin') middlewareapps/api/src/routes/sat.routes.ts— authorize('admin') en cron endpointsapps/api/src/routes/cfdi.routes.ts— Límite de 50MB específico para bulkapps/api/src/services/auth.service.ts— databaseName removido de JWT, email normalizadoapps/api/src/services/usuarios.service.ts— randomBytes + sin console.logapps/api/src/services/email/email.service.ts— requireTLSapps/api/src/services/email/templates/new-client-admin.ts— Sin DB credentials, con escapeHtmlapps/api/src/services/payment/mercadopago.service.ts— Rechazar si no hay secretapps/api/src/config/env.ts— FIEL_ENCRYPTION_KEY requeridoapps/api/src/app.ts— Body limit 10MBpackages/shared/src/types/auth.ts— databaseName removido de JWTPayloaddeploy/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
.enven.gitignorey nunca commiteado