fix(security): bypass de rate limiting via X-Forwarded-For spoofing — VULN: Rate limit evasion

Vulnerabilidad corregida:
- Con 'trust proxy 1', Express usaba X-Forwarded-For como req.ip.
  Un atacante podia cambiar ese header en cada peticion para obtener
  un bucket de rate limit nuevo, bypasseando el limite de 8 busquedas/min.
  Demostrado: 12 requests exitosas cuando el limite era 8.

Mitigacion:
- keyGenerator en todos los rate limiters usa X-Real-IP (establecido por
  Nginx con $remote_addr, no manipulable por el cliente).
- trust proxy cambiado de '1' a 'loopback'.
- Verificado: con X-Real-IP fijo y XFF variando, rate limit actua en req 9.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hacklab 2026-04-07 17:29:50 +02:00
parent f2ff04ecdc
commit 93d75ddafe

View file

@ -12,31 +12,43 @@ const egosearch = require('./routes/egosearch');
const app = express(); const app = express();
app.set('trust proxy', 1); // confía en nginx para obtener la IP real del cliente // trust proxy: loopback para que req.ip use X-Forwarded-For correctamente,
// pero el keyGenerator de rate-limit usa X-Real-IP (imposible de spoofear por clientes)
// porque Nginx lo establece desde $remote_addr (dirección TCP real).
app.set('trust proxy', 'loopback');
app.disable('x-powered-by'); app.disable('x-powered-by');
app.use(helmet()); app.use(helmet());
app.use(express.json({ limit: '10kb' })); app.use(express.json({ limit: '10kb' }));
// Rate limiting general /* Extrae la IP real desde X-Real-IP (puesto por Nginx con $remote_addr).
No puede ser manipulado por el cliente Nginx sobreescribe este header. */
function realIp(req) {
return req.headers['x-real-ip'] || req.ip || '0.0.0.0';
}
// Rate limiting general — usa IP real (anti-bypass X-Forwarded-For)
app.use(rateLimit({ app.use(rateLimit({
windowMs: 15 * 60 * 1000, windowMs: 15 * 60 * 1000,
max: 100, max: 100,
standardHeaders: true, standardHeaders: true,
legacyHeaders: false, legacyHeaders: false,
keyGenerator: realIp,
})); }));
// Rate limiting específico para envío de emails (más estricto) // Rate limiting específico para envío de emails (más estricto)
const emailLimit = rateLimit({ const emailLimit = rateLimit({
windowMs: 60 * 60 * 1000, // 1 hora windowMs: 60 * 60 * 1000, // 1 hora
max: 10, max: 10,
message: { error: 'Demasiadas solicitudes de envío. Espera antes de reintentar.' } message: { error: 'Demasiadas solicitudes de envío. Espera antes de reintentar.' },
keyGenerator: realIp,
}); });
// ── Rate limits específicos ─────────────────────────────────────── // ── Rate limits específicos ───────────────────────────────────────
const searchLimit = rateLimit({ const searchLimit = rateLimit({
windowMs: 60 * 1000, // 1 minuto windowMs: 60 * 1000, // 1 minuto
max: 8, max: 8,
message: { error: 'Demasiadas búsquedas. Espera un momento.' } message: { error: 'Demasiadas búsquedas. Espera un momento.' },
keyGenerator: realIp,
}); });
// ── Rutas ──────────────────────────────────────────────────────── // ── Rutas ────────────────────────────────────────────────────────