Proteger tu WordPress contra spam de búsquedas con Rate Limiting

Si tienes un blog de WordPress, probablemente no te hayas dado cuenta de que el formulario de búsqueda puede ser un vector de ataque. Los bots maliciosos pueden hacer cientos de búsquedas por segundo, sobrecargando tu servidor y afectando el rendimiento para usuarios legítimos.

Hoy te voy a enseñar cómo implementé un sistema de rate limiting (limitación de velocidad) para búsquedas en WordPress, sin necesidad de plugins pesados.

#¿Qué es el Rate Limiting?

El rate limiting es una técnica que limita el número de acciones que un usuario puede realizar en un período de tiempo. En nuestro caso, limitaremos las búsquedas a un máximo razonable para prevenir:

  • Ataques DDoS (Denegación de Servicio Distribuido)
  • Spam de bots automatizados
  • Sobrecarga del servidor por búsquedas masivas
  • Consumo excesivo de recursos de base de datos

#El problema: búsquedas sin control

Por defecto, WordPress no tiene ningún límite en las búsquedas. Esto significa que:

  • Un bot puede hacer 1000 búsquedas en un minuto
  • Cada búsqueda ejecuta queries en la base de datos
  • Tu servidor se sobrecarga procesando peticiones inútiles
  • Los usuarios reales experimentan lentitud

#La solución: Rate Limiting con WordPress Transients

Voy a mostrarte cómo implementar un sistema de rate limiting usando las Transients API de WordPress, que es una forma de caché temporal perfecta para este propósito.

#Paso 1: Crear la función de rate limiting

Abre el archivo functions.php de tu tema y añade esta función al final:

text
/**
 * Rate limiting para búsquedas (prevenir spam)
 */
function mi_tema_search_rate_limit() {
    // Solo aplicar en búsquedas
    if (!is_search()) {
        return;
    }
    
    // Obtener IP del usuario
    $user_ip = $_SERVER['REMOTE_ADDR'];
    
    // Sanitizar IP para usar como clave de transient
    $transient_key = 'search_limit_' . md5($user_ip);
    
    // Obtener número de búsquedas realizadas
    $search_count = get_transient($transient_key);
    
    // Límite: 10 búsquedas por minuto
    $max_searches = 10;
    $time_window = 60; // segundos
    
    if ($search_count === false) {
        // Primera búsqueda, iniciar contador
        set_transient($transient_key, 1, $time_window);
    } elseif ($search_count >= $max_searches) {
        // Límite excedido, mostrar error
        wp_die(
            '<h1>Demasiadas búsquedas</h1>' .
            '<p>Has excedido el límite de búsquedas. Por favor, espera un momento antes de intentarlo de nuevo.</p>' .
            '<p><a href="' . esc_url(home_url('/')) . '">Volver al inicio</a></p>',
            'Límite de búsquedas excedido',
            array(
                'response' => 429,
                'back_link' => true
            )
        );
    } else {
        // Incrementar contador
        set_transient($transient_key, $search_count + 1, $time_window);
    }
}
add_action('template_redirect', 'mi_tema_search_rate_limit', 1);

#Cómo funciona este código

  1. Detecta si es una búsqueda: Solo se activa cuando alguien usa el formulario de búsqueda
  2. Identifica al usuario por IP: Usa la IP para rastrear búsquedas individuales
  3. Usa transients como contador: Guarda temporalmente cuántas búsquedas ha hecho cada IP
  4. Aplica el límite: Si supera 10 búsquedas en 60 segundos, muestra un error
  5. Se resetea automáticamente: Después de 60 segundos, el contador vuelve a cero

#Ventajas de usar Transients

  • No usa la base de datos permanentemente: Los transients se guardan en caché y expiran automáticamente
  • Rendimiento óptimo: Mucho más rápido que guardar en la base de datos
  • Limpieza automática: WordPress elimina los transients expirados automáticamente
  • Compatible con plugins de caché: Funciona con Redis, Memcached, etc.

#Paso 2: Sanitizar las búsquedas (seguridad extra)

Además del rate limiting, es importante limpiar los términos de búsqueda para prevenir ataques de inyección SQL y XSS. Añade esta función:

text
/**
 * Limpiar query de búsqueda para prevenir inyección SQL
 */
function mi_tema_sanitize_search_query($query) {
    if ($query->is_search && !is_admin()) {
        // Limpiar el término de búsqueda
        $search_term = get_search_query();
        
        // Eliminar caracteres peligrosos
        $search_term = strip_tags($search_term);
        $search_term = preg_replace('/[^\p{L}\p{N}\s\-_]/u', '', $search_term);
        
        // Limitar longitud (máximo 100 caracteres)
        $search_term = substr($search_term, 0, 100);
        
        // Actualizar query
        if (!empty($search_term)) {
            $query->set('s', $search_term);
        }
    }
    return $query;
}
add_filter('pre_get_posts', 'mi_tema_sanitize_search_query');

#Qué hace esta función

  • Elimina HTML: Previene ataques XSS (Cross-Site Scripting)
  • Elimina caracteres especiales: Solo permite letras, números, espacios, guiones
  • Limita la longitud: Máximo 100 caracteres para prevenir buffer overflow
  • Soporta Unicode: Funciona con acentos, ñ, y otros caracteres en español

#Personalizar los límites

Puedes ajustar fácilmente los límites según las necesidades de tu sitio:

#Cambiar el número máximo de búsquedas

text
// Cambiar de 10 a 5 búsquedas
$max_searches = 5;

// O permitir más búsquedas
$max_searches = 20;

#Cambiar la ventana de tiempo

text
// 30 segundos en lugar de 60
$time_window = 30;

// 2 minutos (120 segundos)
$time_window = 120;

#Cambiar la longitud máxima de búsqueda

text
// Permitir búsquedas más largas
$search_term = substr($search_term, 0, 200);

#Probar que funciona

Para verificar que el rate limiting está funcionando:

  1. Ve a tu sitio web
  2. Usa el formulario de búsqueda
  3. Repite la búsqueda 10 veces rápidamente
  4. En el intento número 11, deberías ver el mensaje: «Demasiadas búsquedas»
  5. Espera 60 segundos y podrás buscar de nuevo

 

#Mejoras adicionales opcionales

#Excluir usuarios registrados

Si quieres que los usuarios registrados no tengan límite:

text
// Al inicio de la función, añadir:
if (is_user_logged_in()) {
    return; // No aplicar límite a usuarios registrados
}

#Diferentes límites según el rol

text
if (current_user_can('administrator')) {
    return; // Administradores sin límite
}

if (current_user_can('subscriber')) {
    $max_searches = 20; // Suscriptores con límite más alto
}

#Registrar intentos bloqueados

Para monitorear ataques:

text
// Cuando se bloquea, registrar en log
if ($search_count >= $max_searches) {
    error_log('Rate limit excedido - IP: ' . $user_ip . ' - Búsqueda: ' . get_search_query());
    // ... resto del código
}

#Ventajas vs Plugins de seguridad

Aspecto Código personalizado Plugin de seguridad
Rendimiento Muy rápido, código mínimo Más lento, muchas funciones
Personalización Total control Limitado a opciones del plugin
Recursos Mínimo impacto Puede consumir mucha memoria
Mantenimiento No necesita actualizaciones Requiere actualizaciones frecuentes
Costo Gratis Muchos requieren versión premium

#Solución de problemas

#El límite no funciona

Posibles causas:

  • Verifica que guardaste el archivo functions.php
  • Limpia la caché de WordPress si usas un plugin de caché
  • Verifica que no haya errores de PHP (activa WP_DEBUG)

#Los usuarios legítimos se bloquean

Solución:

  • Aumenta el límite a 15 o 20 búsquedas
  • Aumenta la ventana de tiempo a 120 segundos
  • Excluye usuarios registrados del límite

#Sitios detrás de proxy o CDN

Si usas Cloudflare u otro CDN, la IP puede no detectarse correctamente. Usa esto:

text
// Obtener IP real detrás de proxy
if (!empty($_SERVER['HTTP_CF_CONNECTING_IP'])) {
    $user_ip = $_SERVER['HTTP_CF_CONNECTING_IP']; // Cloudflare
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    $user_ip = $_SERVER['HTTP_X_FORWARDED_FOR']; // Otros proxies
} else {
    $user_ip = $_SERVER['REMOTE_ADDR'];
}