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,68 @@
import { baseTemplate } from './base.js';
function escapeHtml(str: string): string {
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
export function newClientAdminEmail(data: {
clienteNombre: string;
clienteRfc: string;
adminEmail: string;
adminNombre: string;
tempPassword: string;
databaseName: string;
plan: string;
}): string {
return baseTemplate(`
<h2 style="color:#1e293b;margin:0 0 16px;">Nuevo Cliente Registrado</h2>
<p style="color:#475569;line-height:1.6;margin:0 0 24px;">
Se ha dado de alta un nuevo cliente en Horux360. A continuación los detalles:
</p>
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;">
<tr>
<td colspan="2" style="background-color:#1e293b;color:#ffffff;padding:12px 16px;font-weight:bold;border-radius:6px 6px 0 0;">
Datos del Cliente
</td>
</tr>
<tr>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;width:40%;">Empresa</td>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.clienteNombre)}</td>
</tr>
<tr>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">RFC</td>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.clienteRfc)}</td>
</tr>
<tr>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">Plan</td>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.plan)}</td>
</tr>
</table>
<table width="100%" cellpadding="0" cellspacing="0" style="margin-bottom:24px;">
<tr>
<td colspan="2" style="background-color:#3b82f6;color:#ffffff;padding:12px 16px;font-weight:bold;border-radius:6px 6px 0 0;">
Credenciales del Usuario
</td>
</tr>
<tr>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;width:40%;">Nombre</td>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.adminNombre)}</td>
</tr>
<tr>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">Email</td>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;color:#1e293b;">${escapeHtml(data.adminEmail)}</td>
</tr>
<tr>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;font-weight:bold;color:#475569;">Contraseña temporal</td>
<td style="padding:10px 16px;border-bottom:1px solid #e2e8f0;">
<code style="background-color:#f1f5f9;padding:4px 8px;border-radius:4px;font-size:14px;color:#dc2626;">${escapeHtml(data.tempPassword)}</code>
</td>
</tr>
</table>
<p style="color:#94a3b8;font-size:12px;margin:0;">
Este correo contiene información confidencial. No lo reenvíes ni lo compartas.
</p>
`);
}