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,26 @@
import { prisma } from '../src/config/database.js';
import { hashPassword } from '../src/utils/password.js';
async function main() {
const ivan = await prisma.user.findUnique({ where: { email: 'ivan@horuxfin.com' }, include: { tenant: true } });
if (!ivan) { console.error('Ivan not found'); process.exit(1); }
console.log('Tenant:', ivan.tenant.nombre, '(', ivan.tenant.id, ')');
const existing = await prisma.user.findUnique({ where: { email: 'carlos@horuxfin.com' } });
if (existing) { console.log('Carlos already exists:', existing.id); process.exit(0); }
const hash = await hashPassword('Aasi940812');
const carlos = await prisma.user.create({
data: {
tenantId: ivan.tenantId,
email: 'carlos@horuxfin.com',
passwordHash: hash,
nombre: 'Carlos Horux',
role: 'admin',
}
});
console.log('Carlos created:', carlos.id, carlos.email, carlos.role);
}
main().then(() => process.exit(0)).catch(e => { console.error(e); process.exit(1); });

View File

@@ -0,0 +1,96 @@
import { emailService } from '../src/services/email/email.service.js';
const recipients = ['ivan@horuxfin.com', 'carlos@horuxfin.com'];
async function sendAllSamples() {
for (const to of recipients) {
console.log(`\n=== Enviando a ${to} ===`);
// 1. Welcome
console.log('1/6 Bienvenida...');
await emailService.sendWelcome(to, {
nombre: 'Ivan Alcaraz',
email: 'ivan@horuxfin.com',
tempPassword: 'TempPass123!',
});
// 2. FIEL notification (goes to ADMIN_EMAIL, but we override for test)
console.log('2/6 Notificación FIEL...');
// Send directly since sendFielNotification goes to admin
const { fielNotificationEmail } = await import('../src/services/email/templates/fiel-notification.js');
const { createTransport } = await import('nodemailer');
const { env } = await import('../src/config/env.js');
const transport = createTransport({
host: env.SMTP_HOST,
port: parseInt(env.SMTP_PORT),
secure: false,
auth: { user: env.SMTP_USER, pass: env.SMTP_PASS },
});
const fielHtml = fielNotificationEmail({
clienteNombre: 'Consultoria Alcaraz Salazar',
clienteRfc: 'CAS200101XXX',
});
await transport.sendMail({
from: env.SMTP_FROM,
to,
subject: '[Consultoria Alcaraz Salazar] subió su FIEL (MUESTRA)',
html: fielHtml,
});
// 3. Payment confirmed
console.log('3/6 Pago confirmado...');
await emailService.sendPaymentConfirmed(to, {
nombre: 'Ivan Alcaraz',
amount: 1499,
plan: 'Enterprise',
date: '16 de marzo de 2026',
});
// 4. Payment failed
console.log('4/6 Pago fallido...');
const { paymentFailedEmail } = await import('../src/services/email/templates/payment-failed.js');
const failedHtml = paymentFailedEmail({
nombre: 'Ivan Alcaraz',
amount: 1499,
plan: 'Enterprise',
});
await transport.sendMail({
from: env.SMTP_FROM,
to,
subject: 'Problema con tu pago - Horux360 (MUESTRA)',
html: failedHtml,
});
// 5. Subscription expiring
console.log('5/6 Suscripción por vencer...');
await emailService.sendSubscriptionExpiring(to, {
nombre: 'Ivan Alcaraz',
plan: 'Enterprise',
expiresAt: '21 de marzo de 2026',
});
// 6. Subscription cancelled
console.log('6/6 Suscripción cancelada...');
const { subscriptionCancelledEmail } = await import('../src/services/email/templates/subscription-cancelled.js');
const cancelledHtml = subscriptionCancelledEmail({
nombre: 'Ivan Alcaraz',
plan: 'Enterprise',
});
await transport.sendMail({
from: env.SMTP_FROM,
to,
subject: 'Suscripción cancelada - Horux360 (MUESTRA)',
html: cancelledHtml,
});
console.log(`Listo: 6 correos enviados a ${to}`);
}
console.log('\n=== Todos los correos enviados ===');
process.exit(0);
}
sendAllSamples().catch((err) => {
console.error('Error:', err);
process.exit(1);
});