Il core di WordPress è stato reinstallato, i plugin confrontati con gli originali, il tema ripulito. Eppure ogni pagina del sito continua a eseguire codice che nessuno trova, perché non sta in nessuno dei file che si controllano. Sta in un file che PHP legge prima di eseguire qualsiasi script, indicato da una riga in un file di configurazione: un .htaccess o un .user.ini che nessuno pensa di aprire.
Una riga di configurazione può caricare una backdoor in ogni richiesta PHP del sito, senza modificare un solo file di WordPress.
In questo articolo vediamo come funziona la direttiva auto_prepend_file e perché si comporta diversamente con .htaccess e con .user.ini, come un .htaccess può trasformare un’immagine in uno script eseguibile, quali regole di rewrite indicano un’infezione e come trovare, verificare e rimuovere queste configurazioni senza lasciare nulla di attivo.
Configurazione che esegue codice
PHP ha due direttive pensate per usi legittimi: auto_prepend_file indica un file da includere automaticamente prima di ogni script, auto_append_file uno da includere dopo. Il file viene incluso come con require, quindi il suo codice viene eseguito con gli stessi privilegi del sito, a ogni richiesta, prima ancora che WordPress carichi wp-config.php. Firewall applicativi, strumenti di profilazione e sistemi di monitoraggio le usano proprio per questo.
Per un attaccante sono ancora più interessanti. Il file incluso può stare ovunque sia leggibile, anche fuori dalla cartella del sito, e può avere qualsiasi estensione: un logo.png in wp-content/uploads che contiene codice PHP viene eseguito senza problemi, perché require non guarda l’estensione. Gli scanner che cercano codice nei soli file .php, o che confrontano core e plugin con gli originali, non vedono né il file né il meccanismo che lo carica. È lo stesso limite di cui abbiamo parlato nell’articolo sui limiti degli scanner a firme.
Queste direttive si possono impostare nel php.ini principale, che di norma solo l’amministratore di sistema può modificare, ma anche a livello di cartella. Il modo dipende da come PHP è collegato al web server, ed è qui che entrano in gioco .htaccess e .user.ini.
auto_prepend_file in .htaccess: il caso mod_php
Quando PHP gira come modulo di Apache (mod_php), le impostazioni si possono cambiare dai file .htaccess con le direttive php_value e php_flag. Come indica la documentazione di PHP, servono i permessi AllowOverride Options o AllowOverride All, e la direttiva vale per la cartella in cui si trova il file e per tutte le sottocartelle. Una riga nel .htaccess della radice del sito basta quindi a coinvolgere tutto WordPress:
# Aggiunto in fondo al .htaccess della radice, dopo il blocco di WordPress
<IfModule mod_php.c>
php_value auto_prepend_file "/home/cliente/public_html/wp-content/uploads/2026/03/logo.png"
</IfModule>
Il blocco <IfModule> non è casuale. Se PHP non gira come modulo, Apache non conosce la direttiva php_value e risponde con un errore 500 a ogni richiesta, con un messaggio nel log degli errori del tipo Invalid command 'php_value'. Chi scrive la backdoor preferisce che non funzioni piuttosto che farsi notare, e la protegge con il test sul modulo. Sui server LiteSpeed, invece, PHP in modalità LSAPI accetta le impostazioni da .htaccess come mod_php, e le stesse righe vanno trattate allo stesso modo.
.user.ini: la stessa backdoor con PHP-FPM
Oggi la configurazione più diffusa è PHP-FPM dietro Apache o NGINX, e lì i php_value nel .htaccess non hanno effetto. PHP in modalità CGI e FastCGI legge però un altro file: il .user.ini. Secondo la documentazione ufficiale, PHP cerca questi file nella cartella dello script richiesto e in tutte quelle superiori fino alla document root, accetta le impostazioni modificabili a livello di cartella o di utente e rilegge i file con la frequenza stabilita da user_ini.cache_ttl, che per impostazione predefinita è di 300 secondi.
; /home/cliente/public_html/.user.ini
auto_prepend_file = "/home/cliente/public_html/wp-content/uploads/2026/03/logo.png"
Due conseguenze pratiche. La prima: un .user.ini nella radice del sito vale per ogni script PHP, compreso wp-admin, ed è sufficiente che l’utente PHP possa scrivere nella document root, cosa molto comune. La seconda riguarda la pulizia: dopo aver rimosso la riga la backdoor può restare attiva fino a cinque minuti, perché i processi PHP-FPM hanno ancora in cache la vecchia configurazione. Chi verifica subito dopo la modifica, vede il codice ancora in esecuzione e conclude che la causa è altrove, perde tempo nella direzione sbagliata.
Va detto che .user.ini con auto_prepend_file non è di per sé un segno d’infezione. Il caso legittimo più comune è il firewall di Wordfence, che per la protezione estesa scrive in .user.ini o in .htaccess una direttiva auto_prepend_file che punta a wordfence-waf.php, come spiega il supporto del plugin. Il controllo da fare è sempre lo stesso: aprire il file indicato e verificare che sia quello che dichiara di essere.
Anche auto_append_file compare nelle infezioni, con uno scopo diverso: il file incluso dopo lo script può aggiungere in fondo all’HTML generato uno script JavaScript o un blocco di link di spam, senza toccare né il tema né il database. Il sintomo è un codice che compare dopo il tag di chiusura </html>, un dettaglio che si nota solo guardando il sorgente della pagina. Vale la pena ricordare, infine, che NGINX non legge i file .htaccess: su un server NGINX con PHP-FPM un .htaccess malevolo è innocuo, mentre un .user.ini funziona perfettamente. Controllare solo il file giusto per il proprio server significa, di fatto, controllare solo metà del problema.
Immagini che diventano script: AddHandler e SetHandler
Il secondo uso malevolo del .htaccess non riguarda PHP ma Apache: dire al web server di trattare come script file che non lo sono. È la tecnica che serve a rendere eseguibile una webshell caricata come immagine attraverso un modulo di upload che controlla solo l’estensione.
# Con mod_php: i .jpg di questa cartella vengono eseguiti come PHP
AddHandler application/x-httpd-php .jpg
AddType application/x-httpd-php .jpg
# Con PHP-FPM: stesso effetto passando i file al socket del pool
<FilesMatch "\.(jpg|png)$">
SetHandler "proxy:unix:/run/php/php8.2-fpm.sock|fcgi://localhost"
</FilesMatch>
Il nome dell’handler cambia da server a server, e sui pannelli di hosting si trovano varianti come application/x-httpd-php82 o application/x-httpd-ea-php82. Con PHP-FPM esiste una difesa in più: l’opzione security.limit_extensions del pool, che secondo la documentazione di PHP-FPM vale per impostazione predefinita .php .phar e fa rifiutare a FPM l’esecuzione di script con altre estensioni. Se qualcuno l’ha allargata per far funzionare un vecchio applicativo, la protezione scompare.
Il rovescio della medaglia è altrettanto frequente. Il .htaccess che blocca l’esecuzione di PHP in wp-content/uploads, di cui abbiamo parlato nell’articolo sulle webshell nella cartella uploads, viene cancellato o sovrascritto dal malware, oppure neutralizzato da un .htaccess in una sottocartella che riabilita il motore con php_flag engine on. Un blocco che nessuno controlla dopo averlo messo è un blocco su cui non si può contare.
Rewrite malevoli e .htaccess che bloccano tutto
Il terzo gruppo di modifiche sfrutta mod_rewrite. Le più note sono le regole condizionali che mandano altrove i visitatori in base al referrer o al tipo di dispositivo, lasciando vedere il sito normale all’amministratore: le abbiamo analizzate nell’articolo sui redirect condizionali. Accanto a queste ci sono regole che servono contenuti diversi ai crawler dei motori di ricerca, tipiche dello spam SEO, e le direttive ErrorDocument che fanno gestire le pagine 404 a uno script PHP nascosto, capace di generare al volo migliaia di pagine di spam su indirizzi che non esistono.
# Esempi di regole iniettate
RewriteCond %{HTTP_REFERER} (google|bing|yahoo) [NC]
RewriteCond %{HTTP_USER_AGENT} (android|iphone|mobile) [NC]
RewriteRule ^(.*)$ https://destinazione.example.invalid/?s=%{HTTP_HOST} [R=302,L]
ErrorDocument 404 /wp-content/languages/themes/cache.php
Un ultimo schema merita attenzione perché è distruttivo. Alcune campagne scrivono in ogni cartella del sito un .htaccess che nega l’accesso a tutti i file PHP tranne un piccolo elenco, che comprende i file della backdoor. Il risultato è un sito che funziona solo in parte, un pannello di amministrazione irraggiungibile e un proprietario che, cercando di sistemare, cancella i .htaccess uno alla volta mentre la backdoor li ricrea. In questi casi i file vanno rimossi tutti insieme, dopo aver fermato ciò che li rigenera, spesso un processo o un cron.
Trovare le direttive sospette
Un’installazione WordPress pulita ha di solito un solo .htaccess nella radice, più quelli creati da plugin di cache e sicurezza in cartelle prevedibili. Il primo passo è quindi elencarli tutti, con dimensione e data, insieme ai .user.ini e agli eventuali php.ini fuori posto. Non ci si può affidare al file manager del pannello o al client FTP: molti nascondono per impostazione predefinita i file che iniziano con un punto, ed è proprio per questo che l’attaccante li sceglie. Da riga di comando:
# Tutti i file di configurazione per cartella, dal più recente
find /home/cliente/public_html \( -name .htaccess -o -name .user.ini -o -name php.ini \) \
-printf '%TY-%Tm-%Td %TH:%TM %8s %p\n' | sort -r
# Direttive che eseguono codice o cambiano gli handler
grep -rnE --include=.htaccess --include=.user.ini --include=php.ini \
'auto_(prepend|append)_file|AddHandler|SetHandler|AddType|php_value|php_flag|ErrorDocument' \
/home/cliente/public_html
# Quanti .htaccess ci sono: decine o centinaia indicano il blocco "a tappeto"
find /home/cliente/public_html -name .htaccess | wc -l
Per ogni auto_prepend_file trovato si apre il file indicato: se contiene codice offuscato, catene di base64_decode ed eval, o semplicemente codice PHP in un file con estensione d’immagine, la risposta è chiara. Conviene cercare anche nella cartella superiore alla document root e nella home dell’utente, dove un .user.ini non viene letto ma il file incluso spesso sì. E su un server condiviso la ricerca va estesa a tutti i siti dello stesso utente, come abbiamo visto per la contaminazione incrociata.
Verificare cosa carica davvero PHP
Trovare i file non basta: bisogna sapere quale configurazione è effettivamente attiva per le richieste web. Qui c’è una trappola comune. php -i e WP-CLI usano PHP da riga di comando, che legge il proprio php.ini e ignora sia .htaccess sia .user.ini: un php -i | grep prepend vuoto non dice nulla sul sito.
Il controllo corretto si fa dal web, con un file temporaneo dal nome non indovinabile, da cancellare subito dopo. In phpinfo() la riga di auto_prepend_file ha due colonne: Master Value è il valore del php.ini del server, Local Value quello effettivo dopo .htaccess e .user.ini. Se differiscono, qualcosa a livello di cartella l’ha cambiato. Più mirato ancora è uno script che stampa solo ciò che serve:
<?php
// verifica-7f3a9c.php, nella radice del sito: aprirlo dal browser e poi cancellarlo
header( 'Content-Type: text/plain' );
echo 'SAPI: ', PHP_SAPI, "\n";
echo 'php.ini: ', php_ini_loaded_file(), "\n";
echo 'user_ini.filename: ', ini_get( 'user_ini.filename' ), "\n";
echo 'auto_prepend_file: ', ini_get( 'auto_prepend_file' ), "\n";
echo 'auto_append_file: ', ini_get( 'auto_append_file' ), "\n";
// Il primo file incluso è l'eventuale prepend, prima di questo script
print_r( get_included_files() );
get_included_files() è particolarmente utile: se prima dello script di verifica compare un altro file, quello è stato caricato automaticamente, qualunque sia il meccanismo. Con PHP-FPM si possono anche confrontare le impostazioni del pool in /etc/php/8.2/fpm/pool.d/ (il percorso varia con la distribuzione), dove un php_admin_value[auto_prepend_file] inatteso indicherebbe una compromissione a livello di sistema, non più del solo sito.
Pulizia e prevenzione
La pulizia segue un ordine preciso. Prima si conservano copie dei file di configurazione e dei file inclusi, con le date originali, perché servono a ricostruire l’attacco. Poi si rimuovono le direttive malevole e i file a cui puntano, insieme a quello che li rigenera: un cron, un mu-plugin, un processo in memoria. Con PHP-FPM si ricarica il pool, o si attendono i 300 secondi di user_ini.cache_ttl, prima di verificare di nuovo con lo script di controllo.
# Copia di prova, con date e permessi
mkdir -p ~/incidente && cp -p public_html/.user.ini ~/incidente/user.ini.radice
cp -p public_html/wp-content/uploads/2026/03/logo.png ~/incidente/
# Rimozione e ricarica di PHP-FPM (nome del servizio secondo la versione)
rm public_html/.user.ini public_html/wp-content/uploads/2026/03/logo.png
systemctl reload php8.2-fpm
La prevenzione lavora su più livelli. Il più efficace è che l’utente con cui gira PHP non possa scrivere i file di configurazione: .htaccess e .user.ini di proprietà di un utente diverso, o almeno non scrivibili, impediscono la modifica anche a una webshell. Su Apache si può restringere AllowOverride alle sole categorie che servono a WordPress, o elencare le direttive ammesse con AllowOverrideList; se il sito non usa .htaccess per nulla, AllowOverride None spegne il problema alla radice. Sui server dove i .user.ini non servono, impostare user_ini.filename a una stringa vuota nel php.ini disattiva del tutto la loro lettura. E security.limit_extensions va lasciato a .php. Queste e altre misure sono raccolte nella checklist di hardening dopo la bonifica.
Le configurazioni malevole sono un buon esempio del perché un’infezione va analizzata a livello di sistema e non solo di applicazione: il codice eseguito sta in un file che WordPress non conosce, caricato da un meccanismo che WordPress non controlla. Nei nostri interventi la verifica della configurazione effettiva di PHP e del web server fa parte della procedura, insieme all’analisi dei file offuscati e dei log. Se sospetti che il tuo sito esegua codice che non riesci a trovare, la bonifica garantita in 6, 12 o 24 ore per installazione WordPress comprende anche questa parte: puoi contattarci descrivendo il tipo di server e di PHP in uso, se lo conosci.