Limitar intentos de inicio de sesión en WordPress
Snippet sin plugin para bloquear temporalmente una IP tras varios intentos fallidos de login, la defensa básica contra ataques de fuerza bruta al wp-login.php.
Por defecto, WordPress no limita cuántas veces puede fallar alguien al iniciar sesión. Eso deja la puerta abierta a ataques de fuerza bruta: un script probando miles de combinaciones de usuario/contraseña contra wp-login.php hasta acertar. Hay plugins dedicados a esto (Limit Login Attempts es el más conocido), pero si prefieres no sumar un plugin más, se puede resolver con un snippet usando transients.
add_filter( 'authenticate', 'comprobar_intentos_login', 30, 3 );
function comprobar_intentos_login( $user, $username, $password ) {
if ( empty( $username ) ) {
return $user;
}
$ip = $_SERVER['REMOTE_ADDR'];
$clave = 'login_intentos_' . md5( $ip );
$intentos = (int) get_transient( $clave );
if ( $intentos >= 5 ) {
return new WP_Error( 'demasiados_intentos', 'Demasiados intentos fallidos. Inténtalo de nuevo en 15 minutos.' );
}
return $user;
}
add_action( 'wp_login_failed', 'registrar_intento_fallido' );
function registrar_intento_fallido( $username ) {
$ip = $_SERVER['REMOTE_ADDR'];
$clave = 'login_intentos_' . md5( $ip );
$intentos = (int) get_transient( $clave );
set_transient( $clave, $intentos + 1, 15 * MINUTE_IN_SECONDS );
}
Cada intento fallido desde una IP suma uno a un contador que expira solo a los 15 minutos. Al llegar a 5 intentos, esa IP queda bloqueada hasta que el contador expire.
Las limitaciones de este enfoque
Si tu web está detrás de un proxy o CDN (Cloudflare, por ejemplo), $_SERVER['REMOTE_ADDR'] puede devolver la IP del proxy en vez de la del atacante real — en ese caso hay que leer la IP real desde la cabecera que use tu proxy (HTTP_CF_CONNECTING_IP en el caso de Cloudflare), o el bloqueo no sirve de nada porque siempre “falla” la misma IP.
Para un volumen de ataques serio, un bloqueo a nivel de servidor (fail2ban, o un firewall como el que menciono en WordPress hackeado: qué hacer y cómo evitarlo) es más robusto que resolverlo solo desde PHP, porque ni siquiera deja que la petición llegue a ejecutar WordPress.