Il reset password è uno dei vettori più trascurati nella sicurezza di un sito WordPress. Mentre il brute force sul login è ampiamente noto e spesso protetto, la pagina wp-login.php?action=lostpassword viene quasi sempre ignorata, ed è proprio quello il target di molti bot automatizzati.
Segue un'analisi di un attacco reale, in cui si mostra come riconoscerlo dai log e come bloccarlo con configurazioni nginx (su Linux Ubuntu).
Come si presenta l'attacco
La prima traccia è solitamente una notifica email da WordPress: qualcuno ha richiesto il reset della password per un account amministratore. Guardando i log nginx appare evidente che non si tratta di un errore umano.
grep -i "lostpassword" /var/log/nginx/admin_sito_access.log | tail -30
Emerge una sequenza meccanica di richieste compresse in pochi secondi:
[14:36:07] GET /wp-login.php?action=lostpassword 200
[14:36:10] POST /wp-login.php?action=lostpassword 302
[14:36:10] GET /wp-login.php?checkemail=confirm 200
[14:36:12] GET /wp-login.php?action=rp&key=xkQvn...&login=nome_utente 302
[14:36:13] GET /wp-login.php?action=lostpassword&error=invalidkey 200
[14:36:14] GET /wp-login.php?action=lostpassword 200
[14:36:15] POST /wp-login.php?action=lostpassword 200
[14:36:16] GET /wp-login.php?action=lostpassword 200
[14:36:17] POST /wp-login.php?action=lostpassword 200
Tre pattern indicativi:
Ciclo continuo
GET e POST si alternano a cadenza di un secondo per minuti interi. Nessun umano farebbe così.
User-agent diverso ad ogni richiesta
Chrome su Windows, poi Firefox su Linux, poi Safari su macOS. I bot cambiano user agent per eludere i filtri basati su UA.
Tentativo immediato della chiave di reset
Il bot invia la POST, riceve il 302 di conferma email e nel giro di 2 secondi chiama l'URL di reset con una chiave. La chiave risulta invalida (error=invalidkey) ma il ciclo riparte senza fermarsi.
Contando le richieste si ha un'idea della scala del problema:
grep "lostpassword" /var/log/nginx/admin_sito_access.log | wc -l
grep "lostpassword" /var/log/nginx/admin_sito_access.log.1 | wc -l
Nell'attacco analizzato: 88 richieste nel giorno corrente, 45 il giorno precedente, alla stessa ora. Un cron job schedulato.
Come il bot enumera i nomi utente
WordPress, che non è proprio un bunker inespugnabile per impostazioni di default, espone i nomi utente in almeno tre punti che vengono spesso ignorati. Cambiare il nome utente admin infatti non basta se questi endpoint rimangono accessibili.
REST API
L'endpoint /wp-json/wp/v2/users restituisce la lista utenti senza autenticazione per default.
curl -s https://tuo-sito.com/wp-json/wp/v2/users | python3 -m json.tool | head -20
Risposta tipica:
[
{
"id": 1,
"name": "Mario Rossi",
"slug": "mario_admin",
"link": "https://example.com/author/mario_admin/"
}
]
Il campo slug corrisponde esattamente al login name usato da WordPress per l'autenticazione.
Redirect delle pagine autore
WordPress risponde a /?author=1 con un redirect 301 verso /author/mario_admin/, rivelando il login name dell'utente con ID 1. Iterando su ?author=1, ?author=2 e così via, un bot enumera tutti gli utenti registrati.
curl -sI "https://tuo-sito.com/?author=1" | grep Location
XML-RPC
Il file xmlrpc.php, abilitato di default, supporta il metodo wp.getUsersBlogs e consente di eseguire un numero illimitato di tentativi di autenticazione in una singola richiesta HTTP, rendendolo più pericoloso della pagina di login classica.
curl -s -o /dev/null -w "%{http_code}" https://tuo-sito.com/xmlrpc.php
200 significa accessibile, 403 significa già bloccato.
Contromisure nginx
Tutte le configurazioni che seguono agiscono prima che la richiesta raggiunga PHP. Questo è il punto più efficiente dove intervenire: un plugin WordPress gira dentro PHP, quindi la richiesta arriva comunque al processo. Il rate limiting a livello nginx blocca il bot prima ancora che venga allocata memoria per PHP.
Rate limiting su wp-login.php
In un file nel contesto http di nginx, ad esempio /etc/nginx/conf.d/wp-security.conf:
limit_req_zone $binary_remote_addr zone=wp_login:10m rate=5r/m;
Questa direttiva definisce una zona chiamata wp_login che traccia gli IP in 10MB di memoria condivisa, con un limite di 5 richieste al minuto per indirizzo IP.
Nel vhost del sito, una location dedicata per wp-login.php:
location = /wp-login.php {
limit_req zone=wp_login burst=3 nodelay;
limit_req_status 429;
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
burst=3 consente un picco iniziale di 3 richieste ravvicinate prima che il limite entri in azione. nodelay fa sì che nginx risponda immediatamente con 429 invece di mettere le richieste in coda. Con questa configurazione, il ciclo del bot che esegue decine di POST al minuto viene interrotto dopo le prime richieste.
Blocco XML-RPC
location = /xmlrpc.php {
deny all;
return 403;
}
Se XML-RPC non viene usato attivamente (Jetpack, app mobile WordPress), bloccarlo rimuove un vettore d'attacco senza impatti funzionali.
Blocco enumerazione utenti via REST API
location ~ ^/wp-json/wp/v2/users {
deny all;
return 403;
}
Questa regola blocca solo l'endpoint della lista utenti, senza interferire con il resto dell'API REST.
Per bloccare anche il redirect ?author=N è necessario intervenire lato WordPress, perché nginx non filtra facilmente sui parametri GET. Un plugin come Stop User Enumeration o codice in functions.php gestisce questo caso.
Applicazione
sudo nginx -t
sudo nginx -s reload
Verifica rapida
Quattro comandi per capire in meno di un minuto la situazione di un sito WordPress.
Richieste su wp-login.php nelle ultime 24 ore:
grep "wp-login" /var/log/nginx/tuo_sito_access.log | wc -l
Verifica se la REST API espone la lista utenti:
curl -s https://tuo-sito.com/wp-json/wp/v2/users | python3 -m json.tool | head -20
Verifica se il redirect autore funziona:
curl -sI "https://tuo-sito.com/?author=1" | grep -i location
Verifica se xmlrpc.php risponde:
curl -s -o /dev/null -w "%{http_code}" https://tuo-sito.com/xmlrpc.php
Segnali d'allarme nei log
| Segnale | Significato |
|---|---|
Decine di POST a wp-login.php in pochi minuti |
Bot attivo |
| User-agent diverso ad ogni richiesta | Evasione filtri per UA |
| Stesso pattern ogni giorno alla stessa ora | Job schedulato |
action=rp&key=... subito dopo checkemail=confirm |
Bot con accesso alla casella email o riutilizzo chiavi precedenti |
FAQ
Il reset password è pericoloso anche se la casella email è sicura?
Se l'email dell'admin non è compromessa, il reset in sé non basta per accedere. L'attacco rimane però un problema: inonda la casella di notifiche inutili, genera carico su PHP e conferma al bot che il nome utente è valido (WordPress risponde in modo diverso a username esistenti e inesistenti).
Un plugin di sicurezza tipo Wordfence è sufficiente?
Wordfence agisce a livello PHP, quindi la richiesta arriva comunque al processo. Per volumi elevati, il rate limiting nginx è più efficiente perché il blocco avviene prima che PHP venga avviato.
Il rate limiting non blocca anche gli utenti legittimi?
Con rate=5r/m burst=3, un utente che dimentica la password e riprova 2-3 volte non ha problemi. Il blocco scatta solo su volumi automatici.
Fail2ban può aiutare?
Sì, ma richiede che i log catturino il vero IP del client. Se il sito è dietro un reverse proxy o CDN, verificare che nginx sia configurato con set_real_ip_from e real_ip_header appropriati, altrimenti fail2ban vedrebbe sempre l'IP del proxy e non potrebbe bannare il vero attaccante.
Riferimenti: