From 93d75ddafe6ead11a18b01948e79d25749c1bd14 Mon Sep 17 00:00:00 2001 From: hacklab Date: Tue, 7 Apr 2026 17:29:50 +0200 Subject: [PATCH] =?UTF-8?q?fix(security):=20bypass=20de=20rate=20limiting?= =?UTF-8?q?=20via=20X-Forwarded-For=20spoofing=20=E2=80=94=20VULN:=20Rate?= =?UTF-8?q?=20limit=20evasion?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- api/app.js | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/api/app.js b/api/app.js index 7e8a69b..ff5fd6d 100644 --- a/api/app.js +++ b/api/app.js @@ -12,31 +12,43 @@ const egosearch = require('./routes/egosearch'); 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.use(helmet()); 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({ windowMs: 15 * 60 * 1000, max: 100, standardHeaders: true, legacyHeaders: false, + keyGenerator: realIp, })); // Rate limiting específico para envío de emails (más estricto) const emailLimit = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hora 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 ─────────────────────────────────────── const searchLimit = rateLimit({ windowMs: 60 * 1000, // 1 minuto max: 8, - message: { error: 'Demasiadas búsquedas. Espera un momento.' } + message: { error: 'Demasiadas búsquedas. Espera un momento.' }, + keyGenerator: realIp, }); // ── Rutas ────────────────────────────────────────────────────────