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,323 @@
# API Reference - Horux360
**Base URL:** `https://horuxfin.com/api`
---
## Autenticación
Todos los endpoints (excepto auth) requieren header:
```
Authorization: Bearer <accessToken>
```
### Rate Limits (por IP)
| Endpoint | Límite | Ventana |
|----------|--------|---------|
| `POST /auth/login` | 10 requests | 15 minutos |
| `POST /auth/register` | 3 requests | 1 hora |
| `POST /auth/refresh` | 20 requests | 15 minutos |
| General `/api/*` | 30 requests/s | burst 50 |
---
## Auth (`/api/auth`)
### `POST /auth/register`
Registra nueva empresa y usuario admin. Provisiona base de datos dedicada.
**Body:**
```json
{
"empresa": { "nombre": "Mi Empresa", "rfc": "ABC123456789" },
"usuario": { "nombre": "Juan", "email": "juan@empresa.com", "password": "min8chars" }
}
```
**Response:** `{ accessToken, refreshToken, user: UserInfo }`
### `POST /auth/login`
```json
{ "email": "usuario@empresa.com", "password": "..." }
```
**Response:** `{ accessToken, refreshToken, user: UserInfo }`
### `POST /auth/refresh`
```json
{ "refreshToken": "..." }
```
**Response:** `{ accessToken, refreshToken }`
### `POST /auth/logout` *(requiere auth)*
```json
{ "refreshToken": "..." }
```
### `GET /auth/me` *(requiere auth)*
**Response:** `UserInfo`
---
## Dashboard (`/api/dashboard`)
### `GET /dashboard/kpis?año=2026&mes=3`
KPIs principales: ingresos, egresos, utilidad, margen, IVA balance, conteo de CFDIs.
### `GET /dashboard/ingresos-egresos?año=2026`
Datos mensuales de ingresos/egresos para gráfica anual.
### `GET /dashboard/resumen-fiscal?año=2026&mes=3`
IVA por pagar, IVA a favor, ISR, declaraciones pendientes, próxima obligación.
### `GET /dashboard/alertas?limit=5`
Alertas activas no resueltas, ordenadas por prioridad.
---
## CFDI (`/api/cfdi`)
### `GET /cfdi?page=1&limit=20&tipo=ingreso&search=...`
Lista paginada de CFDIs con filtros.
### `GET /cfdi/resumen`
Resumen de conteo por tipo y estado.
### `GET /cfdi/emisores`
Lista de emisores únicos.
### `GET /cfdi/receptores`
Lista de receptores únicos.
### `GET /cfdi/:id`
Detalle de un CFDI.
### `GET /cfdi/:id/xml`
XML original del CFDI.
### `POST /cfdi`
Crear un CFDI individual. Sujeto a límite de plan.
### `POST /cfdi/bulk`
Carga masiva de CFDIs. Body limit: 50MB. Sujeto a límite de plan.
### `DELETE /cfdi/:id`
Eliminar un CFDI.
---
## Impuestos (`/api/impuestos`)
### `GET /impuestos/iva?año=2026`
Datos mensuales de IVA (trasladado, acreditable, resultado, acumulado).
---
## Alertas (`/api/alertas`)
### `GET /alertas`
### `POST /alertas`
### `PUT /alertas/:id`
### `DELETE /alertas/:id`
### `PATCH /alertas/:id/read`
### `PATCH /alertas/:id/resolve`
---
## Calendario (`/api/calendario`)
### `GET /calendario?año=2026&mes=3`
### `POST /calendario`
### `PUT /calendario/:id`
### `DELETE /calendario/:id`
---
## Reportes (`/api/reportes`)
### `GET /reportes/flujo-efectivo?año=2026`
### `GET /reportes/impuestos?año=2026`
### `GET /reportes/forecasting?año=2026`
### `GET /reportes/concentrado?año=2026`
---
## Export (`/api/export`)
### `GET /export/cfdis?format=excel&tipo=ingreso`
Exporta CFDIs a Excel o CSV.
---
## FIEL (`/api/fiel`)
### `POST /fiel/upload`
```json
{
"cerFile": "<base64>",
"keyFile": "<base64>",
"password": "..."
}
```
- Archivos max 50KB cada uno
- Password max 256 caracteres
### `GET /fiel/status`
Estado actual de la FIEL configurada.
### `DELETE /fiel`
Eliminar credenciales FIEL.
---
## SAT Sync (`/api/sat`)
### `POST /sat/sync`
Iniciar sincronización manual.
```json
{ "type": "daily", "dateFrom": "2026-01-01", "dateTo": "2026-01-31" }
```
### `GET /sat/sync/status`
Estado actual de sincronización.
### `GET /sat/sync/history?page=1&limit=10`
Historial de sincronizaciones.
### `GET /sat/sync/:id`
Detalle de un job de sincronización.
### `POST /sat/sync/:id/retry`
Reintentar un job fallido.
### `GET /sat/cron` *(admin global)*
Info del job programado.
### `POST /sat/cron/run` *(admin global)*
Ejecutar sincronización global manualmente.
---
## Usuarios (`/api/usuarios`)
### `GET /usuarios`
Usuarios del tenant actual.
### `GET /usuarios/all` *(admin global)*
Todos los usuarios de todas las empresas.
### `POST /usuarios`
Invitar usuario (genera password temporal con `crypto.randomBytes`).
```json
{ "email": "nuevo@empresa.com", "nombre": "María", "role": "contador" }
```
### `PUT /usuarios/:id`
Actualizar usuario (nombre, role, active).
### `DELETE /usuarios/:id`
### `PUT /usuarios/:id/global` *(admin global)*
Actualizar usuario de cualquier empresa.
### `DELETE /usuarios/:id/global` *(admin global)*
---
## Tenants / Clientes (`/api/tenants`) *(admin global)*
### `GET /tenants`
Lista de todos los tenants/clientes.
### `POST /tenants`
Crear nuevo tenant. Provisiona base de datos. Envía email al admin.
```json
{
"nombre": "Empresa Nueva",
"rfc": "ENE123456789",
"plan": "business",
"adminNombre": "Pedro",
"adminEmail": "pedro@nueva.com"
}
```
### `PUT /tenants/:id`
Actualizar tenant (plan, limits, active).
### `DELETE /tenants/:id`
Soft delete — renombra la base de datos a `*_deleted_*`.
---
## Suscripciones (`/api/subscriptions`) *(admin global)*
### `GET /subscriptions/:tenantId`
Suscripción activa del tenant.
### `POST /subscriptions/:tenantId/generate-link`
Generar link de pago MercadoPago.
### `POST /subscriptions/:tenantId/mark-paid`
Marcar como pagado manualmente.
```json
{ "amount": 999 }
```
### `GET /subscriptions/:tenantId/payments`
Historial de pagos.
---
## Webhooks (`/api/webhooks`)
### `POST /webhooks/mercadopago`
Webhook de MercadoPago. Requiere headers:
- `x-signature`: Firma HMAC-SHA256
- `x-request-id`: ID del request
---
## Roles y Permisos
| Rol | Descripción | Acceso |
|-----|-------------|--------|
| `admin` | Administrador del tenant | Todo dentro de su tenant + invitar usuarios |
| `contador` | Contador | CFDI, impuestos, reportes, dashboard |
| `visor` | Solo lectura | Dashboard, CFDI (solo ver), reportes |
### Admin Global
El admin del tenant con RFC `CAS2408138W2` tiene acceso adicional:
- Gestión de todos los tenants
- Suscripciones
- SAT cron
- Impersonación via `X-View-Tenant` header
- Bypass de plan limits al impersonar
---
## Tipos Compartidos (`@horux/shared`)
### UserInfo
```typescript
interface UserInfo {
id: string;
email: string;
nombre: string;
role: 'admin' | 'contador' | 'visor';
tenantId: string;
tenantName: string;
tenantRfc: string;
plan: string;
}
```
### JWTPayload
```typescript
interface JWTPayload {
userId: string;
email: string;
role: Role;
tenantId: string;
iat?: number;
exp?: number;
}
```

View File

@@ -0,0 +1,250 @@
# Guía de Despliegue en Producción - Horux360
## Infraestructura
### Servidor
- **OS:** Ubuntu 24.04 LTS
- **RAM:** 22GB
- **CPU:** 8 cores
- **Dominio:** horuxfin.com (DNS en AWS Route 53)
- **SSL:** Let's Encrypt (certificado real via DNS challenge)
- **IP Interna:** 192.168.10.212
### Stack
| Componente | Tecnología | Puerto |
|-----------|-----------|--------|
| Reverse Proxy | Nginx 1.24 | 80/443 |
| API | Node.js + Express + tsx | 4000 |
| Frontend | Next.js 14 | 3000 |
| Base de datos | PostgreSQL 16 | 5432 |
| Process Manager | PM2 | — |
---
## Arquitectura de Red
```
Internet
Nginx (443/SSL)
├── /api/* → 127.0.0.1:4000 (horux-api)
├── /api/auth/* → 127.0.0.1:4000 (rate limit: 5r/s)
├── /api/webhooks/* → 127.0.0.1:4000 (rate limit: 10r/s)
├── /health → 127.0.0.1:4000
└── /* → 127.0.0.1:3000 (horux-web)
```
---
## PM2 - Gestión de Procesos
### Configuración (`ecosystem.config.js`)
```javascript
module.exports = {
apps: [
{
name: 'horux-api',
interpreter: 'node',
script: '/root/Horux/node_modules/.pnpm/tsx@4.21.0/node_modules/tsx/dist/cli.mjs',
args: 'src/index.ts',
cwd: '/root/Horux/apps/api',
instances: 1,
exec_mode: 'fork',
autorestart: true,
max_memory_restart: '1G',
kill_timeout: 5000,
listen_timeout: 10000,
env: { NODE_ENV: 'production', PORT: 4000 },
},
{
name: 'horux-web',
script: 'node_modules/next/dist/bin/next',
args: 'start',
cwd: '/root/Horux/apps/web',
instances: 1,
exec_mode: 'fork',
autorestart: true,
max_memory_restart: '512M',
kill_timeout: 5000,
env: { NODE_ENV: 'production', PORT: 3000 },
},
],
};
```
### Notas
- La API usa `tsx` en lugar de `tsc` compilado porque `@horux/shared` exporta TypeScript raw (ESM) que `dist/` no puede resolver.
- Next.js usa la ruta directa `node_modules/next/dist/bin/next` porque `node_modules/.bin/next` es un shell script que PM2 no puede ejecutar como script Node.js.
### Comandos Útiles
```bash
pm2 restart all # Reiniciar todo
pm2 logs horux-api # Ver logs del API
pm2 logs horux-web # Ver logs del frontend
pm2 monit # Monitor en tiempo real
pm2 save # Guardar estado actual
pm2 startup # Configurar inicio automático
```
---
## Nginx
### Archivo: `/etc/nginx/sites-available/horux360.conf`
#### Rate Limiting
| Zona | Límite | Burst | Uso |
|------|--------|-------|-----|
| `auth` | 5r/s | 10 | `/api/auth/*` |
| `webhook` | 10r/s | 20 | `/api/webhooks/*` |
| `api` | 30r/s | 50 | `/api/*` (general) |
#### Security Headers
- `Content-Security-Policy`: Restrictivo (`default-src 'self'`)
- `Strict-Transport-Security`: 1 año con includeSubDomains
- `X-Frame-Options`: SAMEORIGIN
- `X-Content-Type-Options`: nosniff
- `Permissions-Policy`: camera, microphone, geolocation deshabilitados
- `Referrer-Policy`: strict-origin-when-cross-origin
#### Body Limits
- Global: `50M` (Nginx)
- API default: `10mb` (Express)
- `/api/cfdi/bulk`: `50mb` (Express route-specific)
### Renovar SSL
```bash
certbot renew --dry-run # Verificar
certbot renew # Renovar
```
---
## PostgreSQL
### Configuración de Rendimiento (`postgresql.conf`)
| Parámetro | Valor | Descripción |
|-----------|-------|-------------|
| `max_connections` | 300 | Para multi-tenant con pools por tenant |
| `shared_buffers` | 4GB | ~18% de 22GB RAM |
| `work_mem` | 16MB | Memoria por operación de sort/hash |
| `effective_cache_size` | 16GB | ~72% de RAM |
| `maintenance_work_mem` | 512MB | Para VACUUM, CREATE INDEX |
| `wal_buffers` | 64MB | Write-ahead log buffers |
### Arquitectura Multi-Tenant
Cada cliente tiene su propia base de datos PostgreSQL:
```
horux360 ← Base central (tenants, users, subscriptions)
horux_cas2408138w2 ← Base del admin global
horux_<rfc> ← Base de cada cliente
```
### Backups
```bash
# Cron job: 0 1 * * * /root/Horux/scripts/backup.sh
# Ubicación: /var/horux/backups/
# Retención: 7 diarios + 4 semanales
```
---
## Variables de Entorno
### API (`apps/api/.env`)
```env
NODE_ENV=production
PORT=4000
DATABASE_URL="postgresql://postgres:<password>@localhost:5432/horux360?schema=public"
JWT_SECRET=<min 32 chars>
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
CORS_ORIGIN=https://horuxfin.com
FRONTEND_URL=https://horuxfin.com
FIEL_ENCRYPTION_KEY=<min 32 chars, REQUERIDO>
FIEL_STORAGE_PATH=/var/horux/fiel
# MercadoPago
MP_ACCESS_TOKEN=<token>
MP_WEBHOOK_SECRET=<secret, REQUERIDO para producción>
# SMTP (Google Workspace)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=ivan@horuxfin.com
SMTP_PASS=<app-password>
SMTP_FROM=Horux360 <ivan@horuxfin.com>
# Admin
ADMIN_EMAIL=carlos@horuxfin.com
```
### Web (`apps/web/.env.local`)
```env
NEXT_PUBLIC_API_URL=https://horuxfin.com/api
```
---
## Directorios Importantes
```
/root/Horux/ ← Código fuente
/var/horux/fiel/ ← Archivos FIEL encriptados (0700)
/var/horux/backups/ ← Backups de PostgreSQL
/etc/nginx/sites-available/ ← Config de Nginx
/etc/letsencrypt/live/ ← Certificados SSL
```
---
## Despliegue de Cambios
```bash
# 1. Pull cambios
cd /root/Horux
git pull origin main
# 2. Instalar dependencias
pnpm install
# 3. Build
pnpm build
# 4. Reiniciar servicios
pm2 restart all
# 5. Si hay cambios en nginx:
cp deploy/nginx/horux360.conf /etc/nginx/sites-available/horux360.conf
nginx -t && systemctl reload nginx
```
---
## Troubleshooting
### API no inicia
```bash
pm2 logs horux-api --lines 50 # Ver logs de error
pm2 restart horux-api # Reiniciar
```
### Puerto en uso
```bash
lsof -i :4000 # Ver quién usa el puerto
kill <PID> # Matar proceso
pm2 restart horux-api
```
### Certificado SSL expirado
```bash
certbot renew
systemctl reload nginx
```
### Base de datos lenta
```bash
sudo -u postgres psql -c "SELECT * FROM pg_stat_activity WHERE state = 'active';"
```

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