Quando un sito WordPress viene infettato, la prima idea di quasi tutti è la stessa: «Abbiamo un backup, ripristiniamo quello di ieri». È una reazione comprensibile, perché il ripristino è veloce, lo offre qualunque pannello di hosting e dà l’impressione di cancellare il problema con un clic. A volte funziona. Molto più spesso il sito torna pulito per qualche giorno, poi i redirect, lo spam o l’avviso di Google ricompaiono, e a quel punto si è persa anche la possibilità di capire che cosa era successo.
Un backup riporta il sito a com’era in un certo momento. Non dice se in quel momento il sito era già compromesso, e non chiude la porta da cui l’attacco è entrato.
In questo articolo vediamo le ragioni tecniche per cui il malware sopravvive al restore, e come usare i backup nel modo giusto: non come cura, ma come strumento di analisi e come base per una ricostruzione controllata.
Il backup potrebbe essere già infetto
Il primo problema è di date. Il giorno in cui ci si accorge dell’infezione non è il giorno in cui l’infezione è avvenuta. Un attacco automatizzato che sfrutta un plugin vulnerabile di solito lascia per prima cosa una backdoor: un file piccolo, spesso offuscato, che non fa niente di visibile e serve solo a rientrare. Il carico vero, cioè lo spam, il redirect o lo skimmer sulla pagina di pagamento, può arrivare giorni o settimane dopo, quando chi controlla la backdoor decide di usarla o la rivende.
Nel frattempo i backup continuano a essere fatti, ogni notte, e ognuno contiene la backdoor dormiente. Chi ripristina «l’ultimo backup in cui il sito funzionava bene» sceglie quasi sempre una copia che sembra pulita solo perché il carico non era ancora stato attivato. Dopo il ripristino la backdoor è di nuovo al suo posto, e chi l’ha installata può rimettere il carico quando vuole: dal suo punto di vista il restore non ha cambiato niente.
A questo si aggiunge la conservazione limitata. Molti hosting tengono sette, quattordici o trenta giorni di copie. Se l’ingresso risale a due mesi prima, può darsi che non esista più nessun backup davvero pulito, e allora il ripristino non è un’opzione: bisogna ripulire la versione attuale o ricostruire da fonti affidabili, cioè il core e i plugin scaricati di nuovo dai rispettivi autori, più i contenuti verificati.
Un backup è pulito solo quando è stato verificato, non perché precede i sintomi: più avanti vediamo come.
File senza database, database senza file
Un sito WordPress è fatto di due parti che vivono in posti diversi: i file sul disco (core, temi, plugin, upload, wp-config.php) e il database. Molti sistemi di backup le salvano e le ripristinano separatamente, e i pannelli di hosting propongono spesso il ripristino dei soli file o del solo database. È facile, sotto pressione, ripristinarne una sola.
Il malware, però, usa entrambe. Se si ripristinano soltanto i file, nel database restano tutte le forme di persistenza che non stanno sul disco:
- utenti amministratori creati dall’attacco, a volte nascosti dall’elenco utenti con un filtro nel codice;
- script iniettati negli articoli, nei widget o nelle opzioni del tema, che producono redirect e spam senza toccare un solo file;
- eventi WP-Cron registrati con nomi plausibili, pronti a eseguire codice non appena qualcosa torna ad agganciarsi all’hook;
- trigger SQL che reinseriscono il codice malevolo ogni volta che un contenuto viene salvato.
Il caso inverso è altrettanto comune: si ripristina il database per eliminare lo spam dagli articoli e si lasciano intatti i file, con le backdoor in wp-content/uploads, nel tema o in mu-plugins e drop-in. Il sito sembra a posto finché la backdoor non riscrive i contenuti.
C’è anche un problema di coerenza: file e database di momenti diversi possono non combaciare. Un plugin aggiornato nei file ma non nel database, o viceversa, può lasciare il sito in uno stato inconsistente, con errori che vengono scambiati per effetti del malware e confondono ulteriormente l’analisi.
La persistenza che vive fuori dalla cartella del sito
Il backup di un sito WordPress contiene, per definizione, il sito: la cartella pubblica e il database. Tutto quello che sta fuori non viene salvato e, soprattutto, non viene sovrascritto dal ripristino. È esattamente lì che le infezioni più resistenti mettono la loro persistenza.
I casi che incontriamo più spesso sono questi:
- il crontab dell’utente di sistema, con una riga che ogni pochi minuti scarica il malware da un server esterno o lo ricostruisce da una copia nascosta: ne abbiamo parlato nell’articolo sulle backdoor nei cron job;
- processi PHP rimasti in memoria, avviati dall’attacco e ancora in esecuzione anche se il file da cui sono partiti è stato cancellato: il ripristino dei file non li ferma, e loro riscrivono i file appena ripristinati (vedi processi PHP «deleted» in memoria);
- gli altri siti dello stesso account: su un hosting condiviso o su un server con più installazioni che girano con lo stesso utente, un sito vecchio e dimenticato può reinfettare tutti gli altri, perché ha i permessi per scrivere nelle loro cartelle;
- file di configurazione di PHP come
.user.iniophp.ininella cartella superiore, con una direttivaauto_prepend_fileche fa eseguire un file prima di ogni pagina; - accessi aggiunti al sistema, come una chiave in
~/.ssh/authorized_keyso un account FTP creato dal pannello con credenziali rubate.
Alcuni controlli rapidi, da eseguire come l’utente del sito o come root, danno una prima idea:
# Attività pianificate dell'utente del sito
crontab -l -u utente-sito
# Processi dell'utente il cui eseguibile o la cui cartella non esistono più
ls -l /proc/[0-9]*/exe /proc/[0-9]*/cwd 2>/dev/null | grep '(deleted)'
# Configurazioni PHP e chiavi SSH nella home, fuori dalla cartella pubblica
find ~utente-sito -maxdepth 3 \( -name '.user.ini' -o -name 'php.ini' \) -ls
cat ~utente-sito/.ssh/authorized_keys
# Altre installazioni WordPress nello stesso account
find ~utente-sito -maxdepth 4 -name wp-config.php
Se anche uno solo di questi punti è compromesso, il restore è inutile: il sito ripristinato viene reinfettato alla prima esecuzione del cron o al primo passaggio del processo in memoria.
La porta d’ingresso è ancora aperta
Supponiamo di avere un backup davvero pulito, precedente all’ingresso, e nessuna persistenza esterna. Il ripristino riporta il sito esattamente com’era prima dell’attacco, e questo significa anche con la stessa versione del plugin vulnerabile che l’ha reso possibile. Gli attacchi ai siti WordPress sono in gran parte automatizzati: gli stessi strumenti che hanno trovato il sito la prima volta lo ritroveranno, spesso nel giro di ore o giorni.
Che il punto d’ingresso sia quasi sempre un’estensione non è un’impressione. Secondo il rapporto State of WordPress Security in 2025 di Patchstack, nel 2024 sono state rese note 7.966 nuove vulnerabilità nell’ecosistema WordPress, il 96% delle quali nei plugin, e il 43% era sfruttabile senza autenticazione. Ripristinare un sito senza aggiornare o rimuovere il componente vulnerabile vuol dire riaprire la stessa porta.
Lo stesso vale per le credenziali. Se l’attacco ha letto wp-config.php, conosce la password del database e le chiavi con cui WordPress firma i cookie di accesso; se è entrato con una password rubata, quella password funziona ancora. Il backup contiene gli stessi salt e gli stessi hash delle password di prima: dopo il ripristino vanno cambiati, come descritto nell’articolo sull’hardening di wp-config.php.
# Stato di aggiornamento di plugin e temi dopo il ripristino
wp plugin list --fields=name,status,version,update_version
wp theme list --fields=name,status,version,update_version
# Nuove chiavi e nuovi salt: tutte le sessioni vengono invalidate
wp config shuffle-salts
# Amministratori e password per le applicazioni da ricontrollare
wp user list --role=administrator --fields=ID,user_login,user_email,user_registered
wp user application-password list 1
Quello che si perde tra il backup e oggi
Un ripristino completo non cancella solo il malware: cancella tutto quello che è successo sul sito dopo la data del backup. Per un blog aggiornato una volta al mese può essere poco. Per un e-commerce significa ordini, clienti registrati, stati di spedizione, recensioni; per un portale editoriale articoli, commenti e modifiche della redazione; per un sito con moduli di contatto richieste che nessuno ha ancora letto.
Se il backup scelto è di tre settimane prima, perché quelli successivi contengono la backdoor, la perdita diventa seria. E recuperare quei dati dal database infetto e reinserirli a mano è un lavoro delicato: bisogna portare nel sito ripristinato i contenuti, non il codice malevolo che potrebbe esserci dentro. Prima di ripristinare conviene quindi estrarre dal database attuale ciò che è cambiato dopo la data del backup, per poterlo esaminare e reimportare in modo selettivo:
# Contenuti creati o modificati dopo la data del backup
wp db query "SELECT ID, post_type, post_status, post_modified, post_title
FROM wp_posts
WHERE post_modified > '2026-09-01' AND post_type NOT IN ('revision')
ORDER BY post_modified"
# Utenti registrati dopo la stessa data
wp user list --fields=ID,user_login,user_email,user_registered \
--orderby=registered --order=DESC
Con WooCommerce e l’archiviazione degli ordini ad alte prestazioni (HPOS) attiva, gli ordini non stanno più in wp_posts ma in tabelle dedicate come wp_wc_orders, e vanno esportati da lì. Ogni contenuto recuperato va controllato prima di reinserirlo: un articolo modificato dall’attacco contiene lo stesso script che si voleva eliminare, e un utente registrato dopo la data del backup potrebbe essere proprio l’amministratore aggiunto da chi è entrato.
Usare i backup come prova, non come cura
I backup hanno un valore enorme in un’analisi forense, a patto di usarli per confrontare invece che per sovrascrivere. Una serie di copie giornaliere è una registrazione dell’evoluzione del sito: confrontandole si può capire quando è comparso il primo file estraneo, quali file sono stati modificati e in che ordine, e quindi qual è l’ultimo backup davvero pulito.
Prima di tutto, però, va salvato lo stato attuale del sito infetto, fuori dalla cartella pubblica. Il ripristino lo sovrascrive, e con lui i file malevoli, le date di modifica e il contenuto del database che servono a ricostruire l’attacco:
# Copia dello stato attuale, prima di qualunque intervento
mkdir -p /root/evidenze
tar -czpf /root/evidenze/sito-$(date +%F).tar.gz -C /var/www/esempio.it .
wp db export /root/evidenze/db-$(date +%F).sql --path=/var/www/esempio.it
Confrontare i file
Il confronto più utile è tra due backup estratti in cartelle separate, oppure tra un backup e il sito attuale. Per il core e per i plugin pubblicati su WordPress.org esistono anche le somme di controllo ufficiali, che segnalano ogni file modificato o aggiunto rispetto alla versione originale:
# Estrarre due backup e confrontarli
mkdir -p /root/confronto/0801 /root/confronto/0901
tar -xzf backup-2026-08-01.tar.gz -C /root/confronto/0801
tar -xzf backup-2026-09-01.tar.gz -C /root/confronto/0901
diff -rq --exclude=cache /root/confronto/0801 /root/confronto/0901
# Verificare un backup estratto contro le somme di controllo ufficiali
cd /root/confronto/0801
wp core verify-checksums --skip-plugins --skip-themes
wp plugin verify-checksums --all --skip-plugins --skip-themes
# File PHP negli upload del backup: non dovrebbero esserci
find /root/confronto/0801/wp-content/uploads -type f -iname '*.php*'
wp core verify-checksums legge la versione dai file e funziona anche senza database; wp plugin verify-checksums invece carica WordPress e ha bisogno di un database raggiungibile con le credenziali del wp-config.php estratto, per esempio importando il dump del backup in un database di prova su un’altra macchina. Le opzioni --skip-plugins e --skip-themes evitano di caricare codice potenzialmente infetto. Le somme di controllo non coprono i plugin e i temi commerciali, che vanno confrontati con una copia scaricata di nuovo dal fornitore, né gli upload e wp-config.php, che vanno letti.
Confrontare il database
Anche i dump del database si possono confrontare, a patto di esportarli con una riga per record: così diff mostra esattamente quali opzioni, utenti o contenuti sono cambiati tra due date. Le tabelle più interessanti sono wp_options, wp_users e wp_usermeta; i trigger vengono esportati per impostazione predefinita, le procedure memorizzate solo con --routines:
# Dump con un INSERT per riga, senza data, per un confronto leggibile
mysqldump --skip-extended-insert --skip-dump-date --routines \
-u utente -p nome_db wp_options wp_users wp_usermeta > oggi.sql
diff backup-0801.sql oggi.sql | less
# Firme tipiche del codice iniettato nel dump
grep -nE 'eval\(|base64_decode|gzinflate|<script|String\.fromCharCode' oggi.sql
Le date di modifica dei file aiutano a restringere la finestra, con una cautela: la data di modifica (mtime) si può falsificare con un semplice touch, la data di cambio di stato dell’inode (ctime) no, almeno non senza privilegi elevati. Se la ctime di un file del core è molto più recente della sua mtime, qualcuno lo ha toccato dopo l’installazione. Per incrociare queste date con le richieste sospette servono i log del server web, un tema che abbiamo approfondito nell’articolo sull’analisi dei log con modelli di IA locali.
La sequenza corretta: ripristinare, pulire, chiudere
Il backup resta uno strumento prezioso, purché sia uno dei passaggi e non l’unico. La sequenza che seguiamo, con piccole varianti da caso a caso, è questa:
- Conservare le prove: copia di file, database e log dello stato attuale, fuori dalla cartella pubblica.
- Trovare l’ingresso e la data: dai log e dal confronto tra backup, capire quale componente è stato sfruttato e quando.
- Scegliere un backup verificato, precedente all’ingresso e non solo ai sintomi; se non esiste, ricostruire da fonti pulite.
- Eliminare la persistenza esterna: cron, processi in memoria, configurazioni PHP, chiavi SSH, altri siti dello stesso account.
- Ripristinare file e database insieme, e ricontrollare il risultato con le somme di controllo e una scansione.
- Correggere la vulnerabilità: aggiornare, sostituire o rimuovere il componente sfruttato prima di rimettere il sito online.
- Cambiare tutte le credenziali: amministratori, database, salt, SFTP e SSH, pannello di hosting, chiavi API salvate nelle opzioni.
- Recuperare i dati persi e monitorare: reimportare in modo selettivo i contenuti successivi al backup e sorvegliare file e log nelle settimane seguenti.
Il passo 4 viene prima del 5 per un motivo pratico: un processo in memoria o un cron ancora attivi riscriverebbero i file nel momento stesso in cui vengono ripristinati. E il passo 6 precede la rimessa online, perché un sito ripristinato con la stessa vulnerabilità è semplicemente un sito in attesa della prossima scansione.
Infine, i backup stessi vanno rivisti. Copie conservate sullo stesso server e scrivibili dall’utente del sito possono essere cancellate o alterate da chi ha preso il controllo; una conservazione più lunga, almeno una copia fuori dal server e non modificabile dall’account del sito, e un controllo periodico di integrità rendono il prossimo incidente molto più semplice da gestire.
Se il sito è già stato ripristinato una o più volte e l’infezione continua a tornare, è il segnale che qualcosa sta fuori dal perimetro del backup. È il tipo di caso che seguiamo abitualmente, a livello di applicazione e di sistema insieme, con tempi di bonifica garantiti di 6, 12 o 24 ore per installazione: se vuoi un parere prima di un nuovo restore, scrivici e guardiamo insieme cosa conservare e da dove partire.