Alessandro Aili - WordPress Developer
Alessandro Aili - WordPress DeveloperPlay Alessandro Aili - WordPress DeveloperDrag Alessandro Aili - WordPress DeveloperView
Alessandro Aili - WordPress Developer
  • Homepage
  • Chi sono
  • Portfolio
  • Aree di Attività
    • Sviluppo di Siti Web Personalizzati
    • Ottimizzazione delle Prestazioni
    • Sicurezza del Sito Web
    • Aggiornamenti e Manutenzione
    • Sviluppo di Temi e Plugin Personalizzati
    • Migrazione e Trasferimento del Sito
  • Blog
  • Contatti
Alessandro Aili - WordPress Developer
Alessandro Aili - WordPress Developer Alessandro Aili - WordPress Developer
  • Homepage
  • Chi sono
  • Portfolio
  • Aree di Attività
    • Sviluppo di Siti Web Personalizzati
    • Ottimizzazione delle Prestazioni
    • Sicurezza del Sito Web
    • Aggiornamenti e Manutenzione
    • Sviluppo di Temi e Plugin Personalizzati
    • Migrazione e Trasferimento del Sito
  • Blog
  • Contatti
MENU Alessandro Aili - WordPress Developer
  • Home
  • Sicurezza
  • Blog
  • Come personalizzare la pagina di reset password in WordPress: logo, email e quello che il reset non chiude

Come personalizzare la pagina di reset password in WordPress: logo, email e quello che il reset non chiude

Alessandro Aili - WordPress Developer
Alessandro Aili WordPress Developer

Data di pubblicazione

23 Settembre 2026

Tempo di lettura

24 minuti

Categoria

Sicurezza

Commenti

0

Schema del reset della password in WordPress: la richiesta, l'email con il link, la schermata per la password nuova e una password delle applicazioni che resta attiva

Per personalizzare la pagina di reset password in WordPress conviene sapere prima che non è una pagina. Sono due schermate di wp-login.php, “Password persa”, dove si chiede il link, e “Reimposta password”, dove si sceglie la password nuova, con un’email in mezzo e una conferma alla fine. Chi le vuole diverse pensa di solito al logo e ai colori, che si cambiano con gli hook del core e poche righe di codice. Il resto chiede più attenzione, perché il reset è una delle porte del login: dice a chiunque se un account esiste, spedisce un’email a ogni richiesta valida e alla fine cambia la password di qualcuno.

Questa guida segue il reset tappa per tappa, e per ognuna dice cosa si può cambiare, con quale hook e cosa conviene lasciare al core. Poi le quattro cose che il reset non fa da solo (rispondere a tutti allo stesso modo, contare le richieste, avvisare l’utente, chiudere le password delle applicazioni) e un capitolo per quando l’email non arriva.

Una nota sulle fonti. Tutto quello che segue l’ho verificato il 23 settembre 2026 sul codice sorgente di WordPress 7.1, sulla documentazione ufficiale di WordPress e di WP-CLI, sulle raccomandazioni dell’OWASP per il recupero della password e sul codice dei plugin citati. Il flusso del reset e tutti i blocchi di codice sono stati eseguiti, non solo riletti, in un WordPress 7.1.2 vero e in italiano (WordPress Playground, PHP 8.5), compresi i comandi di WP-CLI. Dove compare una versione, accanto c’è la data.

Le quattro tappe di un reset in WordPress

Ogni tappa ha i suoi hook e i suoi punti deboli, e conviene guardarle una alla volta. I testi fra virgolette sono quelli della traduzione italiana installata con WordPress 7.1.2.

La richiesta, nella schermata “Password persa”

Il link “Password dimenticata?” sotto il modulo di accesso porta a wp-login.php?action=lostpassword. La schermata ha un campo solo, “Nome utente o indirizzo email”, e il pulsante “Ottieni una nuova password”. Se quello che si scrive non corrisponde a nessun account, la risposta è “Errore: non ci sono account con quel nome utente o quell’indirizzo email.”; se corrisponde, si passa a una pagina che invita a controllare la posta. Il modulo risponde quindi in due modi diversi, e chiunque può usarlo per sapere quali account esistono.

Ogni richiesta andata a buon fine spedisce un’email, senza limiti. Nella prova dieci richieste di fila per lo stesso utente hanno fatto partire dieci email.

L’email con il link

Parte subito, con oggetto “Reimpostazione password per [nome del sito]”. Dice che qualcuno ha chiesto un reset, riporta il nome del sito e il nome utente dell’account, anche a chi aveva scritto soltanto l’email, e contiene il link. Se chi ha fatto la richiesta non era collegato, in fondo aggiunge “Richiesta di reimpostazione password originata dall’indirizzo IP” con l’indirizzo. Il link ha la forma wp-login.php?login=nome&key=…&action=rp&wp_lang=it_IT, e il nome utente sta prima della chiave di proposito. Il codice spiega perché: alcuni nomi utente finiscono con un punto, e un indirizzo che termina con un punto è ambiguo.

Il link, una chiave che vale un giorno e una volta sola

La chiave è una stringa di 20 lettere e cifre prodotta dal generatore casuale sicuro di PHP. Nel database, nel campo user_activation_key dell’utente, finisce il suo hash con accanto l’ora della richiesta; la chiave in chiaro viaggia nell’email e, dopo il clic, resta nel browser di chi l’ha aperta. Vale 24 ore, una durata che si cambia con il filtro password_reset_expiration (dalla 4.3), e si usa una volta sola.

Prima della scadenza l’ho vista annullarsi in tre modi. Chiedendone un’altra, perché ogni richiesta sovrascrive la precedente e funziona solo l’ultimo link. Entrando con la vecchia password, perché un accesso riuscito la cancella. Usandola. Nel codice la cancella anche un cambio dell’email o della password dell’account.

Quando si apre il link succede una cosa che non si vede. WordPress copia nome utente e chiave in un cookie, wp-resetpass- seguito da un codice del sito, che vale solo per wp-login.php, non è leggibile da JavaScript e sparisce alla chiusura del browser, e ricarica subito la pagina senza la chiave nell’indirizzo. Da quel momento la chiave non compare più nella barra del browser. È il passaggio che salta l’esempio eseguito più avanti, scritto per spostare il reset nel tema.

La password nuova, e la conferma

La schermata “Reimposta password” propone una password generata, con l’indicatore di robustezza, i pulsanti “Genera password” e “Salva password” e la casella “Conferma l’uso di una password debole”, che basta spuntare per salvare anche una password debolissima. Al salvataggio compare “La password è stata reimpostata. Accedi”, e nessuno viene fatto entrare: si accede con la password nuova, come sempre. Intanto succedono due cose che l’utente non vede. Parte un’email per l’amministratore del sito, “[nome del sito] Password cambiata”, e le sessioni aperte con la vecchia password smettono di valere.

All’utente non arriva niente.

L’OWASP, la fondazione che pubblica linee guida di sicurezza per le applicazioni web, ha una scheda dedicata a questo flusso, il Forgot Password Cheat Sheet. Messa accanto a WordPress 7.1 dà questa tabella.

Cosa chiede l’OWASPWordPress 7.1Dove si interviene
Chiave casuale, lunga, a uso singolo e con scadenzaSì, 20 caratteri, un uso, 24 orepassword_reset_expiration, per accorciarla
Chiave che non resta nell’indirizzoSì, passa in un cookie e l’indirizzo viene ripulitoDa rifare, se sposti la pagina nel tema
Link costruito dall’indirizzo del sito, non dalla richiestaSìNessun intervento
Nessun accesso automatico dopo il resetSìDa non aggiungere
Sessioni chiuse dopo il resetSì per i browser, no per le password delle applicazioniafter_password_reset
Un avviso all’utente dopo il resetNo, avvisa l’amministratoreafter_password_reset
Stessa risposta, negli stessi tempi, per account esistenti e noNolostpassword_post, per la risposta
Un limite alle richiesteNolostpassword_post, un plugin di sicurezza o il server

Le righe con il no, o con il sì a metà, sono quelle che le sezioni sull’email, su quello che succede dopo il reset e sul modulo sistemano con poche righe di codice. Prima, la parte più visibile: l’aspetto.

Logo, colori e testi: la pagina non carica il tema

La prima cosa che viene in mente è il CSS aggiuntivo del Customizer o dell’editor del sito, e sulle schermate di accesso non arriva. Nella prova il CSS aggiuntivo compariva nella home e in nessuna delle tre schermate di wp-login.php, che caricano solo i fogli di stile del core, fra cui quelli del login, dei pulsanti, dei moduli e delle icone. Niente tema, niente CSS personalizzato, niente di quello che i plugin agganciano a wp_head. Per il reset è un vantaggio, visto che sulla pagina in cui passa una chiave non gira codice del tema.

Per cambiarle servono gli hook del login, che il core ha da anni. login_enqueue_scripts carica un foglio di stile, login_headerurl e login_headertext cambiano il link sopra il modulo, login_message aggiunge testo, login_title cambia il titolo della scheda del browser, che di default finisce con la parola WordPress, e password_hint riscrive il suggerimento sotto la password nuova. Il codice va in un mu-plugin, come qui. Se l’aspetto deve seguire il tema funziona anche nel functions.php di un child theme, con get_stylesheet_directory_uri() al posto di plugins_url() per trovare il file CSS.

<?php
/**
 * Plugin Name: Pagina di reset con il logo del sito
 * Description: Logo, colori e due testi sulle schermate di wp-login.php. Va in wp-content/mu-plugins/ insieme a reset-password.css.
 */

// Il foglio di stile del sito, caricato dopo quello del core ("login").
add_action( 'login_enqueue_scripts', function () {
	wp_enqueue_style( 'reset-password', plugins_url( 'reset-password.css', __FILE__ ), array( 'login' ), '1.0' );

	// Il logo impostato nel sito, se c'è, al posto della W di WordPress.
	$logo = wp_get_attachment_image_src( (int) get_theme_mod( 'custom_logo' ), 'medium' );
	if ( $logo ) {
		wp_add_inline_style(
			'reset-password',
			sprintf(
				'.login h1 a { background-image: url("%s"); background-size: contain; width: 100%%; height: 80px; }',
				sanitize_url( $logo[0] )
			)
		);
	}
} );

// Il titolo della scheda del browser: la schermata e il nome del sito, senza "WordPress" in fondo.
add_filter( 'login_title', function ( $login_title, $title ) {
	return $title . ' &lsaquo; ' . get_bloginfo( 'name', 'display' );
}, 10, 2 );

// Il logo porta alla home e ha il nome del sito come testo: è quello che legge uno screen reader.
add_filter( 'login_headerurl', function () {
	return home_url( '/' );
} );
add_filter( 'login_headertext', function () {
	return get_bloginfo( 'name', 'display' );
} );

// Una riga in più sopra il modulo "Password persa".
add_filter( 'login_message', function ( $message ) {
	global $action;
	if ( 'lostpassword' === $action || 'retrievepassword' === $action ) {
		$message .= wp_get_admin_notice(
			"Il link arriva all'indirizzo email del tuo account. Se non lo trovi, guarda nello spam prima di chiederne un altro: ogni nuova richiesta annulla il link precedente.",
			array( 'type' => 'info', 'additional_classes' => array( 'message' ) )
		);
	}
	return $message;
} );

// Il suggerimento sotto il campo della password nuova: la lunghezza, non i simboli.
add_filter( 'password_hint', function () {
	return 'Suggerimento: usa almeno 15 caratteri, anche una frase. Maiuscole, numeri e simboli non sono obbligatori.';
} );

Tre dettagli, tutti verificati nella prova.

Il logo è quello impostato nel sito, dal blocco Logo o dal Customizer, quindi non c’è un file da copiare e tenere allineato, e se il sito non ne ha uno resta la W di WordPress. Il testo del link è quello che legge uno screen reader. Di default è “Powered by WordPress”, con un link a it.wordpress.org, e sotto un logo diverso fa annunciare il nome sbagliato; dalla 5.2 il vecchio filtro sul title del logo è deprecato per ragioni di accessibilità, a favore del testo del link. La riga aggiunta a “Password persa” spiega un errore che altrimenti non si capisce: chi chiede il link due volte e apre la prima email trova un link non valido, perché la seconda richiesta ha annullato la prima.

Il suggerimento di WordPress sotto la password nuova parla di almeno dodici caratteri e consiglia di mescolare maiuscole, numeri e simboli. Le linee guida del NIST SP 800-63B-4 vanno nella direzione opposta, con almeno 15 caratteri quando la password è l’unico fattore e nessuna regola di composizione imposta. Il testo non obbliga a niente, ma è l’unica cosa che l’utente legge mentre sceglie.

Il foglio di stile va nella stessa cartella. Nella 7.1 il colore dei pulsanti, del bordo del campo attivo e dei link al passaggio del mouse viene da variabili CSS che la pagina di accesso riceve dalla classe admin-color-modern del body, e cambiando quelle si ricolora tutto, compreso l’anello del focus, che resta visibile. L’ho controllato nel browser, spostando il focus sul campo e sul pulsante.

/* reset-password.css, nella stessa cartella del file PHP. */

/* Nella 7.1 pulsanti, link e bordo del campo attivo prendono il colore da queste variabili. */
body.login {
	--wp-admin-theme-color: #1d4e89;
	--wp-admin-theme-color--rgb: 29, 78, 137;
	--wp-admin-theme-color-darker-10: #183f70;
	--wp-admin-theme-color-darker-10--rgb: 24, 63, 112;
	--wp-admin-theme-color-darker-20: #122f54;
	--wp-admin-theme-color-darker-20--rgb: 18, 47, 84;
	background: #f6f4f0;
}

/* Solo le schermate del reset: la richiesta e la password nuova (rp, e resetpass se il modulo torna con un errore). */
.login-action-lostpassword #lostpasswordform,
.login-action-rp #resetpassform,
.login-action-resetpass #resetpassform {
	border-top: 4px solid var(--wp-admin-theme-color);
}

Il blu dell’esempio ha un contrasto di 8,4:1 sul bianco e di 7,6:1 sullo sfondo scelto, sopra il 4,5:1 che le WCAG chiedono per il testo. Se invece riscrivi a mano i colori di .button-primary, l’anello del focus va rivisto, e le due verifiche sono quelle della checklist di accessibilità. Le classi login-action-lostpassword e login-action-rp (login-action-resetpass quando il modulo torna con un errore) toccano solo le schermate del reset, mentre login-action-checkemail è la pagina che invita a controllare la posta. In fondo alle schermate restano il link “← Torna a” seguito dal nome del sito, che si cambia con il filtro login_site_html_link, e, se sul sito sono installate più lingue, il menu “Lingua”, che il filtro login_display_language_dropdown toglie.

L’email: testo, mittente e l’avviso che riceve l’amministratore

L’email predefinita dice l’essenziale e tace due cose che a chi la riceve servono: quanto vale il link, e che chiederne un altro annulla questo. Il filtro retrieve_password_notification_email, dalla 6.0, riceve destinatario, oggetto, testo e intestazioni in un array, e permette di cambiarli insieme.

<?php
/**
 * Plugin Name: Email di reset riscritta
 * Description: Oggetto e testo dell'email per reimpostare la password (filtro retrieve_password_notification_email, dalla 6.0). Il link resta quello del core. File in wp-content/mu-plugins/.
 */

add_filter( 'retrieve_password_notification_email', function ( $email, $key, $user_login, $user ) {
	$sito = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );

	// Lo stesso link che costruisce il core: il nome utente prima della chiave, la lingua in fondo.
	$link = network_site_url( 'wp-login.php?login=' . rawurlencode( $user_login ) . "&key=$key&action=rp", 'login' )
		. '&wp_lang=' . get_user_locale( $user );

	// La durata vera, anche se un filtro l'ha cambiata, scritta in italiano come il resto del testo.
	$secondi = (int) apply_filters( 'password_reset_expiration', DAY_IN_SECONDS );
	$ore     = intdiv( $secondi, HOUR_IN_SECONDS );
	$durata  = $ore > 1 ? "$ore ore" : ( 1 === $ore ? "un'ora" : intdiv( $secondi, MINUTE_IN_SECONDS ) . ' minuti' );

	$email['subject'] = sprintf( 'Reimposta la password del tuo account su %s', $sito );
	$email['message'] = sprintf(
		"Ciao,\n\nqualcuno ha chiesto di reimpostare la password dell'account %1\$s su %2\$s.\n\n"
		. "Se l'hai chiesto tu, apri questo link e scegli la password nuova:\n\n%3\$s\n\n"
		. "Il link resta valido per %4\$s e si usa una volta sola. Se ne chiedi un altro, questo smette di funzionare.\n\n"
		. "Se non l'hai chiesto tu, ignora questa email: la password resta quella di prima.\n",
		$user_login,
		$sito,
		$link,
		$durata
	);

	// Come il core: l'indirizzo di chi ha fatto la richiesta, se non era già dentro.
	if ( ! is_user_logged_in() && ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
		$email['message'] .= sprintf( "\nLa richiesta è partita dall'indirizzo IP %s.\n", $_SERVER['REMOTE_ADDR'] );
	}

	return $email;
}, 10, 4 );

Il link è costruito come lo costruisce il core, con il nome utente prima della chiave e la lingua in fondo, e la durata viene letta dal filtro invece di essere scritta a mano: nella prova l’email diceva “24 ore”, e “un’ora” con il filtro impostato a un’ora. Un dettaglio l’ho scoperto eseguendo il blocco con un utente che ha il profilo in inglese. Il core scrive l’email nella lingua del profilo di chi la riceve, non in quella del sito, e la prima versione di questo blocco calcolava la durata con una funzione di WordPress che segue la stessa regola, così in mezzo al testo italiano compariva “1 day”. Adesso la durata è scritta in italiano come il resto; su un sito con utenti in più lingue, invece, il testo va reso traducibile. Se l’email la deve mandare un servizio esterno, il filtro send_retrieve_password_email, dalla 6.0, la ferma.

La riga con l’indirizzo IP resta, perché aiuta chi riceve l’email a capire se la richiesta era sua. Va letta sapendo da dove viene. È REMOTE_ADDR, quindi su un sito dietro Cloudflare o un altro proxy, se l’indirizzo vero non è ripristinato sul server, riporta quello del proxy. È lo stesso problema del limite ai tentativi, che la guida al login tratta alla voce chi è il visitatore.

Il mittente, se nessuno lo cambia, è “WordPress” con l’indirizzo wordpress@ seguito dal dominio del sito senza www. Il codice stesso avverte che alcuni hosting bloccano le email da quell’indirizzo se la casella non esiste. I filtri wp_mail_from e wp_mail_from_name cambiano indirizzo e nome, ma valgono per tutte le email del sito, e l’indirizzo nuovo deve appartenere a un dominio autenticato. Gmail, nelle sue linee guida per i mittenti, chiede a tutti almeno SPF o DKIM. La strada solida è un invio SMTP autenticato, e la prova resta la stessa, un’email vera cercata in casella e nello spam, come nel controllo 23 della checklist di sicurezza.

Poi c’è l’email che non si vede. A ogni reset completato WordPress scrive all’indirizzo impostato in Impostazioni → Generali come “Indirizzo email di amministrazione”, con oggetto “[nome del sito] Password cambiata” e il nome dell’utente, e salta solo il caso in cui l’utente abbia proprio quell’indirizzo. Su un negozio o su un’area riservata con molti iscritti, ogni reset di ogni cliente diventa un’email per l’amministratore. Si toglie con remove_action( 'after_password_reset', 'wp_password_change_notification' ), oppure si manda altrove con il filtro wp_password_change_notification_email. All’utente invece non arriva niente, ed è una delle caselle vuote della tabella.

Dopo il reset: cosa si chiude e cosa resta aperto

Le sessioni si chiudono da sole

Il cookie con cui WordPress riconosce un utente collegato è firmato con le chiavi di sicurezza del sito e con un frammento dell’hash della password. Cambiata la password cambia l’hash, e ogni cookie emesso prima smette di valere. Nella prova due browser collegati come lo stesso utente sono tornati al login alla prima richiesta dopo il reset. Lo stesso succede cambiando la password dal profilo, dove resta dentro soltanto il browser da cui l’hai cambiata.

Il profilo però non se ne accorge. I record di quelle sessioni restano nel database, e il pulsante “Scollegati da ogni postazione” resta attivo anche quando i cookie non valgono più: premerlo pulisce l’elenco.

Le password delle applicazioni restano valide

Le password delle applicazioni, quelle che il profilo genera per i programmi che si collegano al sito con la REST API o XML-RPC, non dipendono né dalla password dell’account né dalle chiavi di sicurezza. Nella prova una password applicazione creata prima del reset ha continuato a funzionare dopo, sulla REST API e su XML-RPC. Ha continuato a funzionare anche dopo il cambio delle chiavi di sicurezza, che è la mossa consigliata quando si sospetta che qualcuno sia entrato. È la porta da cui chi era dentro resta dentro, e il secondo fattore non la copre, per progetto.

Si chiudono dal profilo dell’utente, nel riquadro “Password delle applicazioni”, con “Revoca tutte le password delle applicazioni”, oppure dal codice qui sotto, al momento del reset. Se il sito non le usa, la cosa più semplice è spegnerle, e con Wordfence nelle impostazioni predefinite della 9.0.1 sono già spente.

Dove mandare l’utente, senza farlo entrare

Dopo il reset WordPress mostra una conferma e un link per accedere, e non fa entrare nessuno. L’OWASP chiede lo stesso, e la ragione è pratica. Il reset prova soltanto che chi lo fa legge quella casella di posta, e un accesso fatto dal codice subito dopo non passa dal modulo di login, dove i plugin di secondo fattore fanno la loro verifica. Se vuoi mandare l’utente altrove, mandalo alla pagina di accesso con un messaggio tuo, come nell’esempio, magari con redirect_to verso l’area riservata, e all’accesso ci pensa lui. L’azione giusta è after_password_reset, che il core fa scattare dopo aver salvato la password nuova.

<?php
/**
 * Plugin Name: Dopo il reset della password
 * Description: Revoca le password delle applicazioni, avvisa l'utente e lo manda alla pagina di accesso con un messaggio. File in wp-content/mu-plugins/.
 */

add_action( 'after_password_reset', function ( $user ) {
	// 1. Le password delle applicazioni non dipendono da quella dell'account: si revocano qui.
	WP_Application_Passwords::delete_all_application_passwords( $user->ID );

	// 2. Un avviso all'utente, che il core non manda: dopo un reset avvisa solo l'amministratore.
	$sito = wp_specialchars_decode( get_option( 'blogname' ), ENT_QUOTES );
	wp_mail(
		$user->user_email,
		sprintf( '[%s] La tua password è stata cambiata', $sito ),
		sprintf(
			"Ciao,\n\nla password dell'account %1\$s su %2\$s è stata appena reimpostata.\n\n"
			. "Se l'hai fatto tu, non devi fare altro.\n"
			. "Se non l'hai fatto tu, scrivi subito a %3\$s: per reimpostarla serviva il link arrivato alla tua casella di posta.\n",
			$user->user_login,
			$sito,
			get_option( 'admin_email' )
		)
	);
}, 20 );

// 3. Alla pagina di accesso, senza far entrare nessuno: solo quando il reset arriva da wp-login.php.
add_action( 'after_password_reset', function () {
	if ( ! did_action( 'login_init' ) ) {
		return;
	}
	wp_safe_redirect( add_query_arg( 'password', 'reimpostata', wp_login_url() ) );
	exit;
}, 99 );

add_filter( 'login_message', function ( $message ) {
	if ( isset( $_GET['password'] ) && 'reimpostata' === $_GET['password'] ) {
		$message .= wp_get_admin_notice(
			'Password reimpostata. Accedi con quella nuova.',
			array( 'type' => 'success', 'additional_classes' => array( 'message' ) )
		);
	}
	return $message;
} );

Il primo blocco riempie due caselle della tabella, perché revoca le password delle applicazioni dell’utente e gli manda un avviso. Il secondo rimanda alla pagina di accesso con un messaggio, ma solo se il reset è passato da wp-login.php: after_password_reset scatta anche nei moduli di reset di alcuni plugin, e un redirect lì romperebbe il loro flusso. Nella prova, dopo il salvataggio della password nuova, il browser è andato a wp-login.php?password=reimpostata con il messaggio, quello collegato prima è stato rimandato al login, la password applicazione è stata rifiutata con un 401 e sono partite due email, quella del core all’amministratore e quella nuova all’utente.

Se un’integrazione usa una password applicazione di quell’utente, come un frontend headless che legge le bozze, dopo il reset va rigenerata.

Il modulo dice chi ha un account, e nessuno conta le richieste

Il modulo “Password persa” risponde in un modo agli account che esistono e in un altro a quelli che non esistono, e le due risposte bastano per sapere quali nomi utente ed email sono registrati. Vietare il reset agli amministratori con il filtro allow_password_reset, che sembra una protezione, peggiora le cose. Nella prova il modulo ha cominciato a rispondere in tre modi: l’errore per gli account inesistenti, la conferma per quelli normali e “La reimpostazione della password non è consentita per questo utente” per gli amministratori, che così diventano i più facili da trovare.

La correzione è rispondere a tutti allo stesso modo, e il posto giusto è lostpassword_post, che scatta prima dell’invio. Il blocco che segue, se l’account non esiste, rimanda alla stessa pagina di conferma che vedrebbe un utente vero, e intanto conta le richieste per indirizzo IP fino a cinque, dato che il core non ha nessun limite.

<?php
/**
 * Plugin Name: Modulo di reset con risposta uguale e limite
 * Description: Il modulo "Password persa" risponde allo stesso modo per gli account che esistono e per quelli che non esistono, e accetta 5 richieste per indirizzo IP; il conteggio si azzera 15 minuti dopo l'ultima. File in wp-content/mu-plugins/.
 */

add_action( 'lostpassword_post', function ( $errors, $user_data ) {
	// Solo il modulo di wp-login.php: non l'invio dalla bacheca, non i moduli dei plugin.
	if ( ! did_action( 'login_init' ) ) {
		return;
	}

	// 1. Il limite: conta tutte le richieste, per account esistenti e no.
	// Dietro Cloudflare o un altro proxy, REMOTE_ADDR va ripristinato a livello di server.
	$chiave    = 'reset_richieste_' . md5( $_SERVER['REMOTE_ADDR'] ?? '' );
	$richieste = (int) get_transient( $chiave );
	if ( $richieste >= 5 ) {
		$errors->add( 'troppe_richieste', '<strong>Errore:</strong> troppe richieste da questo indirizzo. Riprova fra qualche minuto.' );
		return;
	}
	set_transient( $chiave, $richieste + 1, 15 * MINUTE_IN_SECONDS );

	// 2. Account inesistente: la stessa risposta di quando esiste. Il campo vuoto resta un errore.
	if ( ! $user_data && 'empty_username' !== $errors->get_error_code() ) {
		$redirect_to = ! empty( $_REQUEST['redirect_to'] ) ? $_REQUEST['redirect_to'] : 'wp-login.php?checkemail=confirm';
		wp_safe_redirect( $redirect_to );
		exit;
	}
}, 10, 2 );

Nella prova, con il blocco attivo, le risposte per un account esistente e per uno inesistente sono identiche, intestazioni comprese, mentre il campo vuoto resta un errore. Dalla sesta richiesta dallo stesso indirizzo il modulo risponde con l’errore del limite, a tutti, e l’email non parte.

Tre avvertenze.

I tempi di risposta restano diversi, visto che solo per un account vero WordPress genera la chiave e spedisce l’email prima di rispondere, e con un SMTP esterno la differenza si allunga; l’OWASP chiede anche tempi uguali, e questo blocco non li rende uguali. La risposta uguale serve se anche il login non rivela i nomi utente, cosa che si sistema con il filtro login_errors. Dietro Cloudflare o un altro proxy, se l’indirizzo vero non è ripristinato sul server, tutti i visitatori hanno lo stesso IP e il limite li ferma insieme.

Se sul sito c’è già un plugin di sicurezza, prima di aggiungere codice guarda cosa fa su questo modulo. Wordfence (9.0.1) ha un limite dedicato, “Lock out after how many forgot password attempts”, attivo di default a 20 richieste per IP in quattro ore, e la sua documentazione consiglia 5. Con l’opzione che non rivela gli utenti validi, attiva di default anche lei, risponde come se l’account esistesse anche quando non esiste. Limit Login Attempts Security (3.3.9) e Kadence Security (10.0.4), nelle versioni gratuite, il modulo non lo toccano; All-In-One Security (5.4.10) può aggiungergli un CAPTCHA.

Sul server il limite si mette sulle richieste POST a wp-login.php, che comprendono anche quelle del reset. Un sistema anti-bot va messo anche su questo modulo, scelto fra quelli che non chiedono di risolvere un test, perché quelli visivi escludono chi naviga da tastiera; come si sceglie fra reCAPTCHA e le sue alternative lo spiega una guida a parte.

Portare la pagina nel tema: cosa fa il core che dovresti rifare

Si sposta il reset in una pagina del tema per avere intorno al modulo il menu, il footer e il resto del sito, che wp-login.php non ha nemmeno con il logo giusto. Se il sito è un negozio WooCommerce, quella pagina c’è già. Il reset dei clienti sta in “Il mio account”, si personalizza con i modelli form-lost-password.php e form-reset-password.php della cartella myaccount e ha una sua email. Theme My Login (7.2.2, del 15 settembre 2026) fa lo stesso per accesso, registrazione e reset con pagine virtuali, senza creare pagine vere, e nel suo codice gestisce la chiave come il core.

Scriverlo da zero si può, e l’elenco di quello che il core fa sulle schermate del reset spiega perché conviene di rado.

  • Toglie la chiave dall’indirizzo, copiandola in un cookie che vale solo per quella pagina, e ricarica senza.
  • Controlla la chiave due volte, con check_password_reset_key() quando mostra il modulo e quando lo riceve, e confronta quella inviata con hash_equals().
  • Tiene la pagina fuori dai motori e dagli iframe, con noindex, noarchive, una policy sul referrer e le intestazioni che impediscono di caricarla dentro un altro sito. Una pagina del tema va messa in noindex a mano, e con un plugin SEO attivo si fa dal plugin, come spiego in come si mette in noindex una sola pagina.
  • Non finisce in nessuna cache. Manda le intestazioni che la vietano; una pagina del tema va esclusa anche dalla cache di pagina del sito, perché il modulo contiene la chiave in un campo nascosto.
  • Ha già i link giusti. Da una pagina del tema vanno cambiati a mano: i link “Password dimenticata?” con il filtro lostpassword_url, e quello che arriva in casella con il filtro dell’email.
  • Distingue il link scaduto da quello non valido e rimanda alla richiesta con il messaggio giusto.

Un esempio pubblicato come metodo “per sviluppatori”, eseguito su WordPress 7.1.2, mostra cosa succede quando se ne salta metà. Intercetta il link su login_init e manda l’utente a una pagina del tema con nome utente e chiave nell’indirizzo, dove uno shortcode chiama reset_password() passandole il nome utente e la chiave al posto dell’utente e della password nuova. La pagina va in errore fatale (500, “Call to a member function exists() on null”). Un secondo hook, pensato per il redirect dopo il reset, non scatta mai, perché il modulo del core invia a action=resetpass e quell’hook aspetta action=rp. Nel frattempo la chiave sta nell’indirizzo di una pagina dove girano tutti gli script del tema. Con quel codice attivo nessuno riesce più a reimpostare la password, e la funzione wp_reset_password(), nominata nel testo che lo accompagna, nel core non esiste.

Da qui una regola pratica: si sposta l’aspetto e si lascia il meccanismo. Se il reset deve stare nel tema, lo fa un plugin mantenuto che replica il core, e sulla pagina pubblicata si verificano le sei cose dell’elenco. Anche i plugin fatti con cura ci tornano sopra, e la 7.2.2 di Theme My Login, nel changelog, corregge un caso in cui su un multisito una richiesta di reset poteva sovrascrivere la password di un account.

Se l’email non arriva

Quando l’invio fallisce in un modo che WordPress vede, la schermata lo dice: “Errore: l’email non può essere inviata. Il sito potrebbe non essere configurato correttamente per inviare email.”, con un link alla documentazione. Se invece l’invio riesce e l’email finisce nello spam, o viene scartata più avanti, la schermata invita a controllare la posta come sempre. Si parte dalla cartella spam e da un’email di prova mandata dal sito, e se non arriva nemmeno quella il problema è la consegna: l’autenticazione del dominio della sezione sull’email o, dopo un trasloco su un server nuovo, le credenziali SMTP.

Se l’utente deve rientrare subito, le strade dipendono dall’accesso che hai.

  • Un altro amministratore apre il profilo dell’utente da Utenti e usa “Imposta nuova password”, poi salva il profilo: la password cambia senza passare dalla posta. “Invia la reimpostazione della password”, sulla riga dell’utente, rimanda invece la stessa email.
  • Con WP-CLI ci sono i comandi del blocco qui sotto. wp user reset-password non manda un link, imposta una password casuale e, con --show-password, la stampa una volta; senza --skip-email all’utente arriva l’avviso di password cambiata.
  • Con accesso al database, da phpMyAdmin, si scrive la password nuova nel campo user_pass dell’utente scegliendo la funzione MD5. Su WordPress 7.1.2 funziona ancora, e al primo accesso l’hash viene rifatto con bcrypt. La documentazione propone anche un generatore MD5 su un sito esterno, ed è meglio la funzione di phpMyAdmin, che la password non la manda a nessuno.
# Gli amministratori, con nome utente ed email.
wp user list --role=administrator --fields=ID,user_login,user_email

# Una password casuale per l'utente, mostrata una volta, senza email.
wp user reset-password nome-utente --show-password --skip-email

La pagina della documentazione sul reset della password (aggiornata il 27 marzo 2026) va letta con tre cautele, tutte verificate eseguendo. Dice che dopo la richiesta “your new password is emailed to you”, mentre arriva un link. Il suo esempio per WP-CLI, --user_pass=$UP3RstrongP4$w0rd, in una shell diventa --user_pass= perché i $ vengono espansi, e WP-CLI risponde “Success: Updated user 1.” lasciando la password com’era. Una password con il dollaro va fra apici singoli, e resta comunque nella cronologia della shell.

Il metodo via FTP, una riga wp_set_password() in testa al functions.php del tema, cambia la password a ogni caricamento di pagina, e la documentazione della funzione avverte proprio di questo. Ogni cambio invalida il cookie appena ricevuto, quindi con la riga al suo posto l’accesso riesce e la pagina successiva rimanda al login, a ogni tentativo. L’ordine che funziona è l’inverso di quello scritto nella guida: si aggiunge la riga, si apre una pagina qualsiasi del sito, si toglie la riga, e solo dopo si accede.

Come verificare che funzioni

Con un utente di prova e una casella che controlli, in quest’ordine. Chiedi il reset e verifica che l’email arrivi in casella e non nello spam. Aprila, e guarda la chiave sparire dalla barra del browser. Chiedi un secondo link e prova il primo, che deve dare errore. Completa il reset e controlla la schermata finale, l’email all’amministratore e, se hai messo il blocco, quella all’utente. Da un altro browser collegato prima, ricarica la bacheca, che deve chiederti di accedere.

Le risposte del modulo si controllano anche dal terminale.

SITO="https://www.tuosito.it"
LOGIN="$SITO/wp-login.php"   # se la pagina di accesso ha un indirizzo diverso, va quello

# 1. Un account inesistente e il tuo utente di prova: le due righe devono essere uguali.
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" -d "user_login=nessuno-si-chiama-cosi" "$LOGIN?action=lostpassword"
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" -d "user_login=utente-di-prova" "$LOGIN?action=lostpassword"

# 2. Il limite: altre quattro richieste. Con le due di sopra fanno sei, e la sesta va rifiutata.
for i in 1 2 3 4; do
	curl -s -o /dev/null -w "%{http_code} " -d "user_login=nessuno-si-chiama-cosi" "$LOGIN?action=lostpassword"
done; echo

Con il blocco del modulo attivo le prime due righe sono identiche e il ciclo stampa 302 302 302 200. Senza, la prima riga è 200 (la pagina con l’errore), la seconda un 302, e il ciclo stampa quattro volte 200, dato che nessuno conta.

La seconda richiesta manda una vera email all’utente di prova.

Domande frequenti

Quanto dura il link per reimpostare la password in WordPress?

Ventiquattro ore dalla richiesta, e si usa una volta sola. Smette di valere prima se l’utente chiede un altro link, perché ogni richiesta sostituisce la precedente, oppure se entra con la vecchia password, perché un accesso riuscito lo annulla. La durata si cambia con il filtro password_reset_expiration, che riceve i secondi. Se hai riscritto l’email, conviene che il testo legga la durata dallo stesso filtro, così resta giusto quando la cambi.

Cosa vuol dire che il link per reimpostare la password non è valido?

WordPress distingue due casi. Il link scaduto, quando sono passate più di 24 ore dalla richiesta, e il link non valido, che copre gli altri casi: il link è già stato usato, ne è stato chiesto un altro dopo, l’utente è entrato con la vecchia password o ha cambiato l’email dell’account. In tutti e due i casi il modulo propone di chiederne uno nuovo, e va usato l’ultimo arrivato.

Dopo il reset della password gli altri dispositivi restano collegati?

I browser collegati con la vecchia password vengono rimandati al login alla prima richiesta, perché il cookie di accesso è firmato anche con un pezzo dell’hash della password. Restano valide invece le password delle applicazioni, che non dipendono dalla password dell’account né dalle chiavi di sicurezza. Si revocano dal profilo con “Revoca tutte le password delle applicazioni”, oppure in automatico al momento del reset con l’azione after_password_reset.

Perché la pagina “Password dimenticata” dice se un account esiste?

È il comportamento del core, che risponde con un errore agli account che non esistono e con una pagina di conferma a quelli che esistono. Si corregge sull’azione lostpassword_post, mandando alla stessa conferma anche chi scrive un nome inesistente, ed è quello che fa Wordfence con le impostazioni predefinite. Serve se anche il modulo di login non rivela i nomi utente, e non rende uguali i tempi di risposta.

Come cambio il logo nella pagina di reset password?

Con l’azione login_enqueue_scripts e una regola CSS su .login h1 a, più i filtri login_headerurl e login_headertext per il link e per il testo che legge uno screen reader. Il CSS aggiuntivo del tema non basta, perché la pagina di reset non carica il tema.

L’email per reimpostare la password non arriva: cosa faccio?

Prima la cartella spam, poi un’email di prova mandata dal sito. Se non arriva nemmeno quella, il problema è la consegna, e si affronta con un invio SMTP autenticato e con SPF o DKIM sul dominio. Se l’utente deve rientrare subito, un altro amministratore può impostargli una password nuova dal profilo, e con WP-CLI il comando wp user reset-password ne imposta una casuale. Da phpMyAdmin si può scrivere la password nuova nel campo user_pass con la funzione MD5, che WordPress 7.1 accetta ancora.

Da qui in avanti

Il reset di WordPress fa bene la parte difficile: una chiave casuale che scade e vale una volta sola, tenuta fuori dall’indirizzo, e nessun accesso regalato alla fine. Gli manca quello che sta intorno: rispondere a tutti allo stesso modo, contare le richieste, avvisare l’utente e chiudere le password delle applicazioni. Bastano due azioni del core, lostpassword_post e after_password_reset, e il logo resta il più facile dei lavori da fare sulla pagina.

La verifica va rifatta quando cambi plugin di sicurezza o servizio di invio delle email, le due cose che possono cambiare il comportamento del reset senza che la pagina lo mostri, e sta bene accanto al controllo periodico delle email del sito.

Se preferisci che lo faccia io

Sistemare aspetto, email e protezioni del reset, e verificarle da fuori con un utente di prova, è parte del lavoro di messa in sicurezza dei siti WordPress che faccio per i clienti. Se vuoi che me ne occupi io, scrivimi com’è fatto il sito e chi ci accede.

Indice dei contenuti

  • Le quattro tappe di un reset in WordPress
  • Logo, colori e testi: la pagina non carica il tema
  • L’email: testo, mittente e l’avviso che riceve l’amministratore
  • Dopo il reset: cosa si chiude e cosa resta aperto
  • Il modulo dice chi ha un account, e nessuno conta le richieste
  • Portare la pagina nel tema: cosa fa il core che dovresti rifare
  • Se l’email non arriva
  • Come verificare che funzioni
  • Domande frequenti
  • Da qui in avanti
  • Se preferisci che lo faccia io

Tag in Evidenza

HardeningLoginSicurezza WordPressWordPress

Condividi


Lascia un commento Annulla risposta

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *

Alessandro Aili - WordPress Developer Alessandro Aili - WordPress Developer
Sono un WordPress developer che affronta tutte le tue sfide online.
Skills
  • Sviluppo di Siti Web Personalizzati
  • Ottimizzazione delle Prestazioni
  • Sicurezza del Sito Web
  • Aggiornamenti e Manutenzione
  • Sviluppo di Temi e Plugin Personalizzati
  • Migrazione e Trasferimento del Sito
© Copyright 2026 Alessandro Aili
  • Mappa del sito
  • Privacy Policy
  • Cookie Policy
  • Preferenze cookie
Alessandro Aili - WordPress Developer