Contents
Introducción
En este artículo encontrarás un tutorial completo y detallado para limitar los intentos de login en WordPress y registrar las direcciones IP (y metadatos) de los intentos utilizando PHP. El objetivo es proteger tu sitio frente a ataques de fuerza bruta, detectar patrones sospechosos y bloquear temporalmente IPs que abusan del formulario de acceso.
Requisitos y recomendaciones previas
- Acceso por FTP o al gestor de archivos del servidor para subir un plugin o modificar archivos.
- Acceso al panel de administración de WordPress para activar el plugin y ver registros (si se implementa una vista).
- Copia de seguridad de la base de datos antes de crear tablas nuevas o ejecutar cambios en producción.
- Considerar proxies y balanceadores: extraer la IP real de HTTP_X_FORWARDED_FOR cuando proceda.
Estrategia propuesta
A grandes rasgos sigue estos pasos:
- Crear una tabla propia en la base de datos para almacenar cada intento (IP, nombre de usuario, marca temporal, éxito/fracaso, user agent).
- Registrar cada intento: en caso de fallo con wp_login_failed, en caso de éxito con wp_login, y durante el proceso de autenticación para bloquear antes de validar credenciales con authenticate.
- Lógica de bloqueo: usar un sistema de conteo por IP (y opcionalmente por usuario) con ventanas temporales y duración de bloqueo configurables. Se puede implementar con transients de WordPress para eficiencia o con la propia tabla si necesitas histórico a largo plazo.
- Proporcionar whitelist/blacklist y notificaciones al administrador opcionales.
Crear la tabla de registros (SQL)
A continuación tienes la sentencia SQL que crea la tabla que usaremos para almacenar intentos. El código se ejecuta desde la función de activación del plugin (uso de dbDelta).
global wpdb table_name = wpdb->prefix . login_attempts charset_collate = wpdb->get_charset_collate() sql = CREATE TABLE table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, ip VARCHAR(100) NOT NULL, username VARCHAR(100) DEFAULT , attempt_time DATETIME NOT NULL, success TINYINT(1) NOT NULL DEFAULT 0, user_agent TEXT, PRIMARY KEY (id), KEY ip (ip), KEY username (username), KEY attempt_time (attempt_time) ) charset_collate
Plugin ejemplo: código completo (básico y seguro)
Debajo tienes un ejemplo de plugin listo para usar. Crea un archivo PHP en wp-content/plugins (por ejemplo: login-limit-ip-logger.php), pega el contenido y actívalo desde el panel de plugins. Ajusta constantes según tus necesidades.
prefix . login_attempts charset = wpdb->get_charset_collate() sql = CREATE TABLE table ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, ip VARCHAR(100) NOT NULL, username VARCHAR(100) DEFAULT , attempt_time DATETIME NOT NULL, success TINYINT(1) NOT NULL DEFAULT 0, user_agent TEXT, PRIMARY KEY (id), KEY ip (ip), KEY username (username), KEY attempt_time (attempt_time) ) charset require_once( ABSPATH . wp-admin/includes/upgrade.php ) dbDelta( sql ) } / ----- Helper: obtener IP real ----- / function ll_get_ip() { if ( ! empty( _SERVER[HTTP_X_FORWARDED_FOR] ) ) { ips = explode( ,, _SERVER[HTTP_X_FORWARDED_FOR] ) return trim( ips[0] ) } if ( ! empty( _SERVER[HTTP_CLIENT_IP] ) ) { return _SERVER[HTTP_CLIENT_IP] } return _SERVER[REMOTE_ADDR] ?? 0.0.0.0 } / ----- Registrar intento en la tabla ----- / function ll_log_attempt_db( username, success ) { global wpdb table = wpdb->prefix . login_attempts ip = ll_get_ip() ua = isset(_SERVER[HTTP_USER_AGENT]) ? substr(_SERVER[HTTP_USER_AGENT], 0, 255) : wpdb->insert( table, array( ip => ip, username => username, attempt_time => current_time(mysql), success => success ? 1 : 0, user_agent => ua, ), array(%s,%s,%s,%d,%s) ) } / ----- Lógica de conteo y bloqueo con transients ----- / function ll_get_transient_key( ip ) { return LL_TRANSIENT_PREFIX . md5(ip) } function ll_is_whitelisted( ip ) { list = get_option( LL_WHITELIST_OPTION, array() ) if ( ! is_array(list) ) list = array() return in_array( ip, list, true ) } function ll_is_blocked( ip ) { if ( ll_is_whitelisted(ip) ) return false key = ll_get_transient_key(ip) data = get_transient( key ) if ( empty(data) ) return false if ( ! empty( data[blocked_until] ) time() < data[blocked_until] ) { return true } return false } function ll_increment_fail( ip ) { key = ll_get_transient_key(ip) data = get_transient( key ) now = time() if ( empty(data) ! is_array(data) ) { data = array( count => 1, first => now, blocked_until => 0 ) } else { // Si la ventana expiró (first WINDOW), reiniciamos el contador if ( now - data[first] > LL_WINDOW_SECONDS ) { data[count] = 1 data[first] = now data[blocked_until] = 0 } else { data[count] = (data[count] ?? 0) 1 } } // Si supera umbral, bloquea if ( data[count] >= LL_MAX_ATTEMPTS ) { data[blocked_until] = now LL_BLOCK_SECONDS } // Expiración del transient: como mínimo la ventana bloque para mantener estado expire = max( LL_WINDOW_SECONDS, LL_BLOCK_SECONDS ) 60 set_transient( key, data, expire ) return data } function ll_reset_fail( ip ) { key = ll_get_transient_key(ip) delete_transient( key ) } / ----- Hooks para registrar intentos y actualizar contador ----- / add_action(wp_login_failed, ll_on_login_failed) function ll_on_login_failed( username ) { ip = ll_get_ip() // Registrar en DB ll_log_attempt_db( username, false ) // Incrementar contador (transient) ll_increment_fail( ip ) } / En caso de login correcto: resetear contador y registrar éxito / add_action(wp_login, ll_on_login_success, 10, 2) function ll_on_login_success( user_login, user ) { ip = ll_get_ip() ll_reset_fail( ip ) ll_log_attempt_db( user_login, true ) } / ----- Interceptar la autenticación para bloquear antes de comprobar credenciales ----- / add_filter(authenticate, ll_authenticate_block, 30, 3) function ll_authenticate_block( user, username, password ) { ip = ll_get_ip() if ( ll_is_blocked( ip ) ) { return new WP_Error( ll_blocked, __( Demasiados intentos de acceso. Inténtalo más tarde. ) ) } return user } / ----- Función útil para mostrar registros (ejemplo simple para admins) ----- / function ll_get_last_attempts( limit = 50 ) { global wpdb table = wpdb->prefix . login_attempts return wpdb->get_results( wpdb->prepare( SELECT ip, username, attempt_time, success, user_agent FROM table ORDER BY attempt_time DESC LIMIT %d, limit ) ) } ?>
Explicación paso a paso del código
- Creación de tabla: la función de activación usa dbDelta para crear una tabla personalizada con columnas para IP, usuario, marca temporal, éxito y user agent.
- Detección de IP: ll_get_ip() intenta leer HTTP_X_FORWARDED_FOR (para proxys) y luego REMOTE_ADDR.
- Registro: cada fallo dispara wp_login_failed y añade una fila a la tabla cada éxito usa wp_login para anotar el intento exitoso y resetear los contadores.
- Control de bloqueo: se usan transients (clave por IP) para almacenar un array con count, first (marca temporal de la primera petición en la ventana) y blocked_until. Si count alcanza LL_MAX_ATTEMPTS se fija blocked_until = ahora LL_BLOCK_SECONDS.
- Prevención antes de autenticar: el filtro authenticate (prioridad 30) verifica si la IP está bloqueada y retorna un WP_Error antes de que WordPress compruebe credenciales.
Ajustes recomendados y aspectos de seguridad
- Whitelist: añade IPs administrativas o de monitorización al array de whitelist para evitar bloqueos accidentales.
- Considerar proxies legítimos: si tu servidor está detrás de un proxy o CDN (Cloudflare, etc.), configura correctamente la obtención de la IP real para evitar bloquear todo el tráfico desde la IP del proxy.
- Evitar logging excesivo: la tabla puede crecer implementa tareas CRON para purgar registros antiguos (por ejemplo, > 90 días) y/o archivar registros.
- Notificaciones: puedes agregar envío de correo al administrador cuando una IP es bloqueada repetidamente o cuando se detectan múltiples IPs desde la misma subred.
- Protección adicional: combinar con reCAPTCHA en wp-login.php o con soluciones de WAF/Firewall en el servidor para mayor seguridad.
- Prevención de bypass: los atacantes pueden rotar IPs considera también limitar por nombre de usuario y enviar alertas ante enumeración de usuarios.
Cómo visualizar los registros (ejemplo mínimo)
En el ejemplo incluí una función ll_get_last_attempts() que devuelve los últimos registros. Para crear una pantalla administrativa podrías añadir un menú en el panel de administración y mostrar una tabla con esos resultados (verifica capacidades con current_user_can(manage_options)).
Mejoras y extensiones posibles
- Implementar almacenamiento en Redis o memcached para conteos si hay mucho tráfico.
- Crear una interfaz de administración para visualizar y filtrar logs, gestionar whitelist/blacklist y desbloquear IPs manualmente.
- Alertas automáticas vía Slack/Telegram/email cuando se detectan patrones anómalos.
- Registro de geolocalización aproximada por IP (usando servicios externos) para análisis forense.
- Política de backoff exponencial: incrementar duración de bloqueo según número total de bloqueos previos.
Consideraciones finales
El enfoque presentado es sencillo de implementar y efectivo para mitigar ataques de fuerza bruta basados en IP. Aun así, combina esta protección con otras capas (firewall, reCAPTCHA, monitorización) para lograr una seguridad robusta. Realiza pruebas en un entorno de staging antes de desplegar en producción y ajusta los parámetros (intentos, ventana, duración de bloqueo) según el tráfico y las necesidades de tu sitio.
Recursos útiles
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |