Skip to content

Helmet, CORS y rate limit ​

Tres middlewares que toda API en producción debería tener desde el primer día:

  • helmet: añade cabeceras HTTP de seguridad (contra clickjacking, sniffing de MIME, etc.).
  • cors: decide qué dominios pueden llamar a tu API desde el navegador.
  • express-rate-limit: limita cuántas peticiones puede hacer una IP en un periodo (frena fuerza bruta y abusos).

Instalación ​

bash
npm install helmet cors express-rate-limit
env
CORS_ORIGINS=http://localhost:5173,https://www.minegocio.mx,https://admin.minegocio.mx

Configuración centralizada ​

js
import helmet from 'helmet';
import cors from 'cors';
import rateLimit from 'express-rate-limit';

// ---------- CORS ----------
const origenes = (process.env.CORS_ORIGINS ?? '').split(',').map((o) => o.trim()).filter(Boolean);

export const corsMiddleware = cors({
  origin(origin, callback) {
    // Sin "origin" = peticiones del mismo servidor, curl, Postman o apps móviles
    if (!origin || origenes.includes(origin)) return callback(null, true);
    callback(new Error(`Origen no permitido por CORS: ${origin}`));
  },
  credentials: true,                // permite cookies (necesario para refresh tokens)
  methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  maxAge: 86400,                    // cachea el preflight 24 h
});

// ---------- Helmet ----------
export const helmetMiddleware = helmet({
  // Content Security Policy: de dónde se pueden cargar scripts, estilos, imágenes...
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'", 'https://fonts.googleapis.com'],
      fontSrc: ["'self'", 'https://fonts.gstatic.com'],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'"],
    },
  },
  crossOriginResourcePolicy: { policy: 'cross-origin' }, // permite mostrar /uploads en otro dominio
});

// ---------- Rate limit ----------
const mensaje = (texto) => ({ error: texto });

/** General: 300 peticiones cada 15 minutos por IP */
export const limiteGeneral = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 300,
  standardHeaders: 'draft-7', // envía RateLimit-* para que el cliente sepa cuánto le queda
  legacyHeaders: false,
  message: mensaje('Demasiadas peticiones, intenta en unos minutos'),
});

/** Login/registro: 5 intentos fallidos cada 15 minutos */
export const limiteAuth = rateLimit({
  windowMs: 15 * 60 * 1000,
  limit: 5,
  skipSuccessfulRequests: true, // sólo cuentan los intentos fallidos
  standardHeaders: 'draft-7',
  legacyHeaders: false,
  message: mensaje('Demasiados intentos de inicio de sesión. Espera 15 minutos.'),
});

/** Formularios públicos (contacto, reservas): 10 por hora */
export const limiteFormularios = rateLimit({
  windowMs: 60 * 60 * 1000,
  limit: 10,
  message: mensaje('Has enviado demasiados formularios. Intenta más tarde.'),
});

Aplicarlo en app.js ​

js
import express from 'express';
import { corsMiddleware, helmetMiddleware, limiteGeneral, limiteAuth, limiteFormularios } from './middlewares/seguridad.js';

const app = express();

app.set('trust proxy', 1);     // IMPRESCINDIBLE detrás de Nginx/Render/Cloudflare para que el límite use la IP real
app.disable('x-powered-by');   // no anuncies que usas Express

app.use(helmetMiddleware);
app.use(corsMiddleware);
app.use(express.json({ limit: '100kb' })); // limita el tamaño del cuerpo
app.use('/api', limiteGeneral);

// Límites específicos ANTES de las rutas
app.use('/api/auth/login', limiteAuth);
app.use('/api/auth/registro', limiteAuth);
app.use('/api/contacto', limiteFormularios);

// ... rutas

Checklist de seguridad para tu API ​

✔Medida
☐HTTPS en producción (Let's Encrypt / Cloudflare)
☐helmet() activo y x-powered-by deshabilitado
☐CORS con lista blanca, nunca origin: '*' con credenciales
☐Rate limit general y uno estricto en login
☐Límite de tamaño del body (express.json({ limit }))
☐Validación de entrada con zod
☐Consultas preparadas (sin concatenar SQL)
☐Contraseñas con bcrypt/argon2
☐Errores sin stack trace en producción (middleware de errores)
☐.env fuera de Git y secretos largos
☐npm audit periódico y dependencias actualizadas

Error común

Olvidar app.set('trust proxy', 1) detrás de un proxy. Todas las peticiones parecen venir de la misma IP (la del proxy), así que el rate limit bloquea a todos tus usuarios a la vez.

Varios servidores

express-rate-limit guarda los contadores en memoria. Si ejecutas varias instancias (PM2 en modo cluster, varios contenedores), usa un store compartido como rate-limit-redis para que el límite sea global.

Academia Web — aprende, copia y construye.