Lo staging nasce con le migliori intenzioni: una copia del sito dove provare aggiornamenti, un nuovo tema o un plugin prima di toccare la produzione. Poi il progetto finisce, la copia resta. Nessuno la aggiorna, nessuno ne controlla gli accessi, e il suo nome, staging.esempio.it o dev.esempio.it, è da qualche parte nei registri pubblici. Quando analizziamo come è entrato un attaccante in un sito curato e aggiornato, la risposta è sorprendentemente spesso: non dal sito, ma dalla sua copia.
Un ambiente di staging ha quasi sempre gli stessi dati e le stesse chiavi della produzione, e quasi mai le stesse difese. È il punto debole che rende inutili le difese del sito principale.
In questo articolo vediamo come gli ambienti di sviluppo e staging diventano visibili dall’esterno, perché sono più deboli della produzione e quali legami tra i due ambienti vanno recisi. L’obiettivo è pratico: rendere lo staging non enumerabile, proteggerlo con un’autenticazione a livello di server prima ancora che mostri WordPress e impedire che condivida credenziali e dati con la produzione. Gli esempi di configurazione per NGINX e Apache sono pensati per essere applicati così come sono.
Come diventano visibili gli ambienti «che nessuno conosce»
La prima fonte è la Certificate Transparency. Le autorità di certificazione pubbliche registrano ogni certificato emesso in log pubblici e a sola aggiunta, che chiunque può consultare. Nel momento in cui si attiva HTTPS su staging.esempio.it, per esempio con Let’s Encrypt, quel nome diventa pubblico per sempre. Nell’articolo sul DNS hijacking e il takeover dei sottodomini abbiamo usato questi log per fare l’inventario dei propri sottodomini; è utile ripetere l’interrogazione proprio per scoprire quali ambienti di prova si sono resi pubblici senza accorgersene.
# I propri nomi nei log dei certificati che somigliano ad ambienti di prova
curl -s "https://crt.sh/?q=%25.esempio.it&output=json" \
| jq -r '.[].name_value' | tr 'A-Z' 'a-z' | sort -u \
| grep -E '(^|[.-])(dev|staging|stage|test|beta|demo|old|preprod|uat)[0-9]*([.-]|$)'
La seconda fonte è il DNS. Anche senza certificati, i nomi più comuni per un ambiente di prova sono un elenco corto e prevedibile, facile da provare uno per uno: chi cerca uno staging parte proprio da dev, test, staging2 o nuovo. La terza sono i motori di ricerca: uno staging raggiungibile e linkato anche una sola volta, da un’email, da un ticket pubblico o da un’immagine caricata con l’indirizzo sbagliato, finisce nell’indice con tutte le sue pagine. Come vedremo, noindex tiene le pagine fuori dai risultati ma non impedisce a nessuno di raggiungere il sito: non è una misura di sicurezza.
Stesso IP, Host diverso: i virtual host affiancati
C’è un modo per raggiungere un ambiente che non compare né nei certificati né nel DNS pubblico. Un server web decide quale sito servire leggendo l’intestazione Host della richiesta, un valore che il client sceglie liberamente. Conoscendo l’indirizzo del server di produzione, si possono inviare allo stesso IP richieste con Host: staging.esempio.it e confrontare le risposte: se lo staging è servito dallo stesso server, risponde anche quando il suo nome risolve solo nel file hosts dello sviluppatore. Il fatto che un nome non sia pubblico, quindi, non lo mette al riparo se vive sullo stesso indirizzo della produzione.
Il problema si aggrava con il virtual host predefinito, quello che risponde quando l’intestazione Host non corrisponde a nessun sito. In NGINX, come spiega la documentazione sull’elaborazione delle richieste, è il primo server configurato per quella porta, salvo diversa indicazione con default_server; in Apache, secondo la documentazione sui virtual host, è il primo virtual host elencato. Se per caso è lo staging, una richiesta all’IP nudo lo mostra. E sulla porta 443 il certificato consegnato a chi si collega senza indicare un nome può elencare nei propri nomi alternativi proprio gli ambienti che si volevano tenere riservati.
Per capire come appare il proprio server visto da fuori, conviene interrogarlo come farebbe un estraneo: chiedere una pagina con un Host inesistente e vedere se il server chiude la connessione o serve comunque qualcosa, e leggere i nomi nel certificato predefinito.
# Cosa risponde il server a un Host sconosciuto: dovrebbe chiudere, non servire un sito
curl -skI -H 'Host: inesistente.invalid' https://203.0.113.10/ | head -3
# Quali nomi rivela il certificato consegnato senza indicazione del nome (SNI)
openssl s_client -connect 203.0.113.10:443 -noservername </dev/null 2>/dev/null \
| openssl x509 -noout -ext subjectAltName
Perché lo staging è più debole della produzione
Lo staging è per definizione un ambiente in cui si abbassano le difese per lavorare meglio. Il debug è attivo, spesso con gli errori mostrati a schermo o scritti in un debug.log raggiungibile dal web, uno dei casi descritti nell’articolo sui file esposti. WordPress stesso, quando wp_get_environment_type() restituisce development, imposta WP_DEBUG a vero se non è già definito. Gli aggiornamenti automatici sono spenti per non disturbare i test, i plugin restano fermi alla versione dell’ultimo lavoro e dopo qualche mese sono vulnerabili.
Poi ci sono gli utenti. Account amministrativi creati per il cliente, per il grafico, per il collega in ferie, con password deboli scelte «tanto è solo lo staging»; nessun secondo fattore per la stessa ragione; e tutti gli utenti della produzione, perché lo staging è una copia del database, con gli stessi hash delle password. Se la copia arriva da un e-commerce, contiene anche nomi, indirizzi, telefoni e ordini dei clienti reali. A quel punto lo staging non è un ambiente di prova: è un secondo archivio di dati personali con le difese di un ambiente di prova, con tutto ciò che questo comporta anche in termini di obblighi sul trattamento dei dati.
I legami che uniscono staging e produzione
Il motivo per cui una copia debole mette a rischio il sito principale è che i due ambienti quasi mai sono davvero separati. Il legame più pericoloso sono le credenziali del database: molte procedure di clonazione copiano il wp-config.php così com’è, quindi lo staging usa lo stesso utente e la stessa password della produzione, spesso sullo stesso server MySQL. In questa configurazione il file di configurazione dello staging è, di fatto, la chiave del database di produzione.
Il secondo legame sono le chiavi e i salt. WordPress firma i cookie di autenticazione con le costanti AUTH_KEY, SECURE_AUTH_KEY e le altre di wp-config.php: se staging e produzione le condividono, i due ambienti si «fidano» delle stesse sessioni, un problema di cui parliamo anche a proposito delle chiavi e dei salt. Il terzo è il filesystem: quando lo staging è una sottocartella della produzione, o vive nella stessa home dello stesso utente di sistema, i due ambienti si scrivono a vicenda, ed è la contaminazione incrociata che abbiamo descritto per gli hosting condivisi. A questi si aggiungono le chiavi di deploy e gli accessi SSH riutilizzati tra gli ambienti.
La conseguenza pratica è netta: separare gli ambienti non è un dettaglio di ordine, è ciò che impedisce a un problema nello staging di diventare un problema in produzione. Ogni ambiente deve avere il proprio utente del database con privilegi limitati al proprio schema, i propri salt, il proprio utente di sistema e, idealmente, il proprio server dei dati.
Regola uno: non essere enumerabile
Il primo obiettivo è ridurre le tracce. Per gli ambienti raggiungibili solo dal team, meglio un nome che risolve nella VPN o nel file hosts invece che nel DNS pubblico, ed evitare i certificati con carattere jolly su cui poi si appoggia lo staging: un certificato per staging.esempio.it lo annuncia nei log di Certificate Transparency, mentre un jolly *.esempio.it non elenca i singoli nomi ma copre qualunque sottodominio, spostando il problema. Dove serve un certificato pubblico, conviene sapere che comparirà nei log e proteggere comunque l’ambiente ai livelli successivi.
Il secondo obiettivo è chiudere il server predefinito, così che una richiesta con un Host sconosciuto non riveli nulla. In NGINX si definisce un server default_server che rifiuta la connessione: la documentazione descrive il codice non standard 444, che chiude la connessione senza inviare una risposta, e dalla versione 1.19.4 esiste ssl_reject_handshake, che rifiuta l’handshake TLS senza presentare alcun certificato.
# NGINX: server predefinito che non rivela nulla su Host o certificati sconosciuti
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_reject_handshake on; # da NGINX 1.19.4
}
Regola due: autenticazione a livello di server
La regola più importante è che uno staging non deve mostrare WordPress a chi non è autenticato a livello di server, prima e in aggiunta al login di WordPress. Il login applicativo non basta: espone comunque la pagina di accesso agli attacchi automatici, lascia raggiungibili l’API REST, xmlrpc.php e i file dei plugin, e non protegge un debug.log o un .git esposto. Un filtro davanti a tutto il sito riduce la superficie a un solo controllo.
Le opzioni, dalla più semplice alla più robusta, sono l’autenticazione HTTP di base, una lista di indirizzi ammessi, una VPN e il mutual TLS con certificato client. La base è sufficiente per la maggior parte degli staging; una lista di IP o una VPN sono la scelta migliore quando l’ambiente serve solo al team; il certificato client è il livello più forte, adatto agli ambienti che espongono dati reali. Nulla vieta di combinarle.
# NGINX: il virtual host dello staging richiede autenticazione prima di tutto
server {
listen 443 ssl;
server_name staging.esempio.it;
# Solo la rete dell'ufficio o la VPN, più l'autenticazione di base
allow 203.0.113.0/24;
allow 10.8.0.0/24;
deny all;
auth_basic "Staging riservato";
auth_basic_user_file /etc/nginx/staging.htpasswd;
# ... resto della configurazione di WordPress ...
}
# Apache 2.4: stesso principio nel VirtualHost o in un .htaccess protetto
<Location "/">
AuthType Basic
AuthName "Staging riservato"
AuthUserFile "/etc/apache2/staging.htpasswd"
Require valid-user
Require ip 203.0.113.0/24
</Location>
# Generare le credenziali: htpasswd -B -c /etc/apache2/staging.htpasswd team
Due avvertenze. La password di base va comunque presa sul serio, perché protegge dati veri, e va cambiata quando qualcuno lascia il progetto. E l’autenticazione a livello di server deve valere per tutto, API REST e xmlrpc.php compresi: se il filtro è messo solo sulle pagine e non sulle chiamate, resta una porta aperta.
Regola tre: dati e segreti separati
Anche protetto e non enumerabile, uno staging non dovrebbe custodire i segreti della produzione. Al momento della clonazione vanno rigenerati i salt, cambiato l’utente e la password del database, e resa l’installazione consapevole di essere staging, così che WordPress e i plugin si comportino di conseguenza. Con WP-CLI sono pochi comandi.
# Dopo aver clonato in staging: nuovi salt e ambiente dichiarato
wp config shuffle-salts
wp config set WP_ENVIRONMENT_TYPE staging --type=constant
# Utente MySQL dedicato allo staging, limitato al suo schema
mysql -e "CREATE USER 'wp_stg'@'localhost' IDENTIFIED BY 'password-robusta';
GRANT ALL PRIVILEGES ON wp_staging.* TO 'wp_stg'@'localhost';"
wp config set DB_USER wp_stg
wp config set DB_PASSWORD 'password-robusta'
Il punto più delicato sono i dati personali. Uno staging usato per lo sviluppo non ha bisogno degli indirizzi e degli ordini reali dei clienti: prima di lavorarci, i dati andrebbero anonimizzati, sostituendo email, nomi e recapiti con valori fittizi e svuotando le tabelle degli ordini. Così, se l’ambiente viene comunque raggiunto, non c’è una violazione di dati personali da gestire. E vale la pena ribadirlo: noindex e Disallow nel robots.txt tengono le pagine fuori dai motori di ricerca, ma la documentazione di Google è esplicita nel dire che il robots.txt non è un sistema per tenere una pagina fuori dall’indice e che, per non farla raggiungere, servono la protezione con password o il noindex. Nessuno dei due impedisce a un estraneo di aprire il sito: solo l’autenticazione a livello di server lo fa.
Se lo staging era esposto
Scoprire uno staging dimenticato e aperto non è diverso dal trovare un file esposto: rimuovere o chiudere non basta, prima bisogna capire se è stato usato. Se lo staging condivideva le credenziali del database con la produzione, quelle credenziali vanno considerate compromesse e cambiate, i salt rigenerati, e la produzione controllata come dopo qualsiasi incidente: utenti amministrativi non riconosciuti, opzioni modificate, attività pianificate, file cambiati di recente. I log di accesso dello staging, se conservati, dicono chi lo ha raggiunto e cosa ha chiesto; quelli della produzione dicono se da lì è partito qualcosa. La priorità è chiara: prima si taglia il legame tra i due ambienti, poi si verifica il sito principale.
Se hai un ambiente di staging o di sviluppo che non sai se è raggiungibile dall’esterno, o se sospetti che un’infezione in produzione sia arrivata da una sua copia, possiamo aiutarti a ricostruire il percorso. Analizziamo virtual host, DNS, certificati e log, cerchiamo i legami tra gli ambienti e ripuliamo ogni installazione WordPress coinvolta, con una pulizia garantita in 6, 12 o 24 ore per installazione, lasciando le regole di blocco al loro posto. Se vuoi un controllo, scrivici.