Middleware de Tarpitting en Express.js: Mitigación Dinámica de Bots con Delays Asíncronos

JAVASCRIPT 18 de junio de 2026 167 lecturas
Implementa una estrategia avanzada de Tarpitting en Node.js y Express.js para ralentizar de forma asíncrona a bots maliciosos mediante retardos dinámicos y respuestas simuladas de éxito (HTTP 200 OK) sin saturar el Event Loop.

En el ecosistema moderno de desarrollo web con Node.js y Express.js, la protección de puntos de entrada públicos —como endpoints de inicio de sesión, formularios de registro, pasarelas de pago y formularios de contacto— suele abordarse mediante respuestas de bloqueo inmediato como HTTP 429 Too Many Requests o HTTP 403 Forbidden. Sin embargo, en mi trayectoria gestionando infraestructura crítica expuesta a tráfico malicioso, he comprobado que este enfoque tradicional presenta deficiencias estructurales significativas frente a botnets avanzadas y herramientas de automatización modernas.

Cuando un bot automatizado recibe un código de error explícito como el HTTP 429 o 403 de forma instantánea, el script atacante identifica de inmediato que su fingerprint, dirección IP o patrón de cabeceras ha sido detectado por las defensas del servidor. Esto desencadena mecanismos automáticos de evasión: rotación inmediata de proxies residenciales de alto rango, modificación del User-Agent, alteración de algoritmos de TLS fingerprinting o inyección de payloads alternativos. Al proporcionar retroalimentación inmediata sobre la eficacia de nuestras reglas de seguridad, estamos asistiendo involuntariamente al atacante en la optimización de su ataque de fuerza bruta o scraping.

Arquitectura de la Solución: Fundamentos del Tarpitting Asíncrono

Para invertir la asimetría de costos entre el atacante y el servidor defensivo, aplico el concepto de Tarpitting (o pozo de alquitrán). Esta técnica de decepción en ciberseguridad consiste en ralentizar deliberadamente la transmisión de datos HTTP de entrada o salida. En lugar de cerrar la conexión o responder con un rechazo inmediato, el servidor acepta la conexión pero aplica un retraso controlado y calculado mediante la pila asíncrona de Node.js antes de emitir una respuesta.

La arquitectura del Event Loop de Node.js resulta excepcionalmente idónea para la implementación de tarpits de nivel de aplicación. Al utilizar primitivas no bloqueantes (setTimeout envuelto en promesas de ECMAScript), la suspensión temporizada de la respuesta no bloquea el hilo principal de ejecución ni detiene el procesamiento de peticiones legítimas entrantes. El estado de la petición suspendida permanece almacenado en la memoria RAM del proceso de Node.js ocupando un footprint insignificante (unos pocos kilobytes por socket de red activo), mientras que el atacante se ve obligado a mantener abiertos hilos de ejecución, sockets TCP y descriptores de archivos durante un intervalo prolongado.

Estrategia de Engaño: La Inyección de Falsos Positivos de Éxito (HTTP 200 OK)

El segundo pilar fundamental de este middleware es la decepción mediante respuestas simétricas plausibles. Si limitamos la respuesta retrasada a un error genérico o a un timeout de red, el software de automatización puede marcar la IP objetivo como inestable y reintentar la operación por otra vía. En su lugar, el middleware de Tarpitting devuelve una respuesta HTTP 200 OK perfectamente formateada con un identificador único de trazabilidad ficticio (UUIDv4) y una estructura JSON idéntica a la que generaría una transacción exitosa.

Esta simulación produce varios efectos cruciales en la infraestructura atacante:

  • Neutralización del consumo de recursos downstream: La petición maliciosa es interceptada y abortada en la capa de middleware perimetral de Express. En ningún momento la solicitud llega a ejecutar consultas a la base de datos (como motores ORM Prisma o TypeORM), ni activa llamadas a microservicios externos o pasarelas de correo electrónico.
  • Contaminación de la métrica del atacante: El bot registra la operación como exitosa en sus logs internos, deteniendo la ejecución del vector de ataque para esa credencial o payload específico bajo la premisa de que el objetivo ha sido cumplido.
  • Agotamiento de pool de conexiones cliente: Si un atacante lanza 5,000 peticiones concurrentes y cada una se mantiene retenida durante 10 a 15 segundos en el tarpit, la capacidad del bot para mantener sockets activos colapsa por límites del sistema operativo cliente o agotamiento de su propio pool de recursos.

Desafíos Técnicos de Implementación y Límites Operativos

A pesar de sus inmensas ventajas, la implementación de un middleware de tarpitting en entornos de producción exige una calibración minuciosa de la arquitectura de red subyacente. Existen factores determinantes que deben ser considerados para garantizar la resiliencia del sistema:

En primer lugar, se debe prestar atención a los timeouts de los proxies inversos y balances de carga de la infraestructura (tales como Nginx, HAProxy, AWS Application Load Balancer o Cloudflare). Si el retraso inyectado por el tarpit excede el proxy_read_timeout del proxy inverso o el timeout de origen de Cloudflare (habitualmente fijado en 30 o 60 segundos), el proxy cortará la conexión emitiendo un HTTP 504 Gateway Timeout. Por ello, el parámetro de retardo máximo (maxDelayMs) de nuestro middleware debe configurarse siempre por debajo de estos umbrales críticos de infraestructura (por ejemplo, entre 8,000 ms y 15,000 ms).

En segundo lugar, se debe prevenir la acumulación excesiva de descriptores de archivos (File Descriptors) en la máquina host de Node.js. Aunque el consumo de CPU es nulo durante la espera asíncrona, cada socket TCP abierto consume memoria de kernel. Para mitigar esto en entornos de tráfico masivo, el middleware debe combinarse con un sistema de puntuación de confianza (Risk Assessment Score) que evalúe indicadores como reputación de IP, fingerprint de TLS, presencia de cabeceras de bot conocidas, y desvíos en el análisis de comportamiento o captchas invisibles (como Google reCAPTCHA v3 o Cloudflare Turnstile).

Casos de Uso Principales e Integración Perimetral

Esta solución técnica es óptima para su despliegue en escenarios con alta exposición a abuso automatizado:

  • Endpoints de autenticación y recuperación de credenciales: Disuade ataques de Credential Stuffing y fuerza bruta ralentizando exponencialmente las pruebas del atacante sin bloquear usuarios legítimos que puedan compartir direcciones IP públicas (NAT corporativo o redes móviles).
  • Formularios de registro de usuarios y lead generation: Previene la creación masiva de cuentas fantasma y el spam de formularios sin comprometer la experiencia de conversión de usuarios reales.
  • APIs de consulta de alta frecuencia: Protege la infraestructura interna contra scraping agresivo de datos sensibles o catálogos de productos.

A continuación se presenta la implementación modular, tipada y refactorizada del middleware de Tarpitting inteligente para Express.js, diseñada siguiendo las mejores prácticas de producción, soporte para proxies confiables y evaluación desacoplada del nivel de riesgo.

const express = require('express');
const { randomUUID } = require('crypto');

const app = express();

// Habilitar trust proxy para la correcta resolución de req.ip detrás de Nginx/Cloudflare/AWS ALB
app.set('trust proxy', true);

app.use(express.json());

/**
 * Función auxiliar para generar un retraso asíncrono no bloqueante con soporte para cancelación.
 * @param {number} ms - Tiempo de retardo en milisegundos.
 * @param {AbortSignal} [signal] - Señal para abortar el temporizador si el cliente desconecta.
 * @returns {Promise<void>}
 */
const delay = (ms, signal) => {
    return new Promise((resolve, reject) => {
        if (signal?.aborted) {
            return reject(new Error('Petición abortada antes de iniciar el retardo.'));
        }

        const timer = setTimeout(() => {
            resolve();
        }, ms);

        if (signal) {
            signal.addEventListener('abort', () => {
                clearTimeout(timer);
                reject(new Error('Cliente desconectado durante la fase de tarpit.'));
            }, { once: true });
        }
    });
};

/**
 * @typedef {Object} TarpitOptions
 * @property {number} [baseDelayMs=3000] - Retraso base inicial en milisegundos.
 * @property {number} [maxDelayMs=12000] - Retraso máximo tolerable por la infraestructura.
 * @property {number} [criticalScore=0.80] - Umbral crítico para activar tarpitting (0.0 a 1.0).
 * @property {(req: import('express').Request) => number | Promise<number>} [scoreEvaluator] - Estrategia de evaluación de score.
 */

/**
 * Fábrica de middleware de Tarpitting Avanzado para Express.js.
 *
 * @param {TarpitOptions} options
 * @returns {import('express').RequestHandler}
 */
const createTarpitMiddleware = (options = {}) => {
    const {
        baseDelayMs = 3000,
        maxDelayMs = 12000,
        criticalScore = 0.80,
        scoreEvaluator = (req) => req.body?.spamScore ?? 0.0
    } = options;

    return async (req, res, next) => {
        const requestId = randomUUID();
        req.requestId = requestId;

        try {
            // Evalúa el nivel de sospecha de la petición
            const spamScore = await scoreEvaluator(req);

            if (spamScore >= criticalScore) {
                // Cálculo de retardo escalar proporcional al nivel de sospecha
                const normalizedRatio = Math.min((spamScore - criticalScore) / (1 - criticalScore), 1.0);
                const calculatedDelay = Math.round(baseDelayMs + normalizedRatio * (maxDelayMs - baseDelayMs));

                console.warn(
                    `🚨 [Tarpit Activado] ID: ${requestId} | IP: ${req.ip} | Score: ${spamScore.toFixed(2)} | Retardo: ${calculatedDelay}ms`
                );

                // Vinculación de evento de cierre de socket para evitar desperdicio de timers
                const ac = new AbortController();
                const onClose = () => ac.abort();
                req.once('close', onClose);

                try {
                    await delay(calculatedDelay, ac.signal);
                } finally {
                    req.removeListener('close', onClose);
                }

                // Intercepción defensiva: FALSO 200 OK sin invocar next()
                return res.status(200).json({
                    status: 'success',
                    message: 'Solicitud procesada correctamente.',
                    refId: requestId,
                    timestamp: new Date().toISOString()
                });
            }

            // Petición legítima: procede al siguiente controlador de la ruta
            return next();
        } catch (error) {
            if (res.headersSent) {
                return;
            }
            if (error.message?.includes('desconectado') || error.message?.includes('abortada')) {
                console.info(`ℹ️ [Tarpit Cancelado] El cliente cerró la conexión TCP. ID: ${requestId}`);
                return;
            }
            return next(error);
        }
    };
};

// --- EJEMPLO DE DESPLIEGUE EN RUTA ---

// Middleware simulador de parámetros para pruebas rápidas
app.use('/api/v1/contacto', (req, res, next) => {
    if (req.query.testSpamScore) {
        req.body = req.body || {};
        req.body.spamScore = parseFloat(req.query.testSpamScore);
    }
    next();
});

// Endpoint protegido con Tarpit
app.post(
    '/api/v1/contacto',
    createTarpitMiddleware({
        baseDelayMs: 4000,
        maxDelayMs: 14000,
        criticalScore: 0.85,
        scoreEvaluator: (req) => {
            const scoreFromBody = req.body?.spamScore || 0;
            const userAgent = req.headers['user-agent'] || '';
            const isKnownBotUA = /python|curl|wget|httpx/i.test(userAgent);
            return isKnownBotUA ? Math.max(scoreFromBody, 0.95) : scoreFromBody;
        }
    }),
    (req, res) => {
        // Lógica de negocio real (solo alcanzada si no fue atrapado en el Tarpit)
        console.log(`[ID: ${req.requestId}] 📦 Guardando payload legítimo en PostgreSQL...`);

        return res.status(201).json({
            status: 'success',
            message: 'Mensaje legítimo recibido y guardado con éxito.'
        });
    }
);

// Ruta de salud de la aplicación
app.get('/api/v1/health', (req, res) => {
    res.status(200).json({ status: 'ok', uptime: process.uptime() });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
    console.log(`🛡️ Búnker de seguridad perimetral escuchando en puerto ${PORT}`);
});
¿Qué te pareció?
🔥 Brillante 0
💡 Me sirvió 0
🚀 A otro nivel 0

¿Te resultó útil este snippet? Explora más código y soluciones en AndresSY.dev.

Volver a Snippets