Stamattina ho chiesto al mio sito l’elenco dei file caricati, senza login, con una sola richiesta all’API REST di WordPress. Ha risposto con 1.056 file. Di questi, 992 non sono collegati a nessun articolo: dentro ci sono 32 video e un PDF del 2020. Nessuno li ha pubblicati di proposito. Sono semplicemente finiti nella Libreria media, e per WordPress questo basta a renderli leggibili da chiunque.
Il 25 settembre 2026 OpenAI ha ammesso un problema con la stessa forma, su scala diversa. Secondo TechCrunch, alcuni agenti AI nel suo ambiente di ricerca hanno caricato 53 immagini fornite dagli utenti su siti di image hosting, con link «non elencati pubblicamente». Le immagini restavano comunque trovabili. Ne avevo scritto nel roundup sulla supply chain dei fornitori. Qui mi interessa il meccanismo, perché è identico a quello che trovo sui siti WordPress delle PMI: un file finisce in un posto pubblico per impostazione predefinita, e qualcuno scambia un indirizzo difficile da indovinare per un controllo di accesso.
Questa è una guida pratica per capire cosa espone il tuo sito, misurarlo in un quarto d’ora e chiudere quello che va chiuso. Vale anche, e soprattutto, se sul sito lavora un agente AI con i permessi di caricamento.
Nascosto non vuol dire privato: la regola da tenere a mente
Un link «non in elenco» fa una cosa sola: non compare negli indici del servizio che lo ospita. Non chiede a nessuno chi sei. Chi ha l’indirizzo apre il file, e gli indirizzi viaggiano: finiscono nelle email di notifica, nei log, negli strumenti di analytics, nelle cronologie dei browser, nelle chat inoltrate, negli strumenti che un agente usa per svolgere il suo compito.
Il controllo di accesso è un’altra cosa. Il server riceve la richiesta, verifica chi la fa e decide se servire il file. Se questo passaggio non esiste, il file è pubblico. Il nome lungo o la cartella con un hash rendono solo più difficile trovarlo per tentativi.
La documentazione di Gravity Forms lo scrive in modo esplicito, ed è la frase più onesta che conosco sull’argomento: gli URL di download sono offuscati per renderli difficili da indovinare, ma non sono sottoposti a controllo di accesso. Chiunque abbia l’URL corretto scarica il file senza autenticarsi.
Cosa espone WordPress senza login
Su un’installazione standard ci sono tre livelli di esposizione, e conviene distinguerli perché si chiudono in modi diversi.
1. Il file stesso
Tutto quello che carichi finisce in wp-content/uploads/, organizzato per anno e mese. Il server web serve quei file direttamente, senza passare da WordPress. Lo stato dell’articolo a cui l’immagine appartiene non conta: bozza, privato o cestinato, l’URL del file risponde lo stesso. WordPress entra in gioco solo per le pagine, non per i file statici.
2. L’elenco dei file via API REST
L’endpoint /wp-json/wp/v2/media restituisce l’elenco degli allegati con il loro source_url, paginato fino a 100 per richiesta, con il totale nell’intestazione X-WP-Total. La regola che decide chi vede cosa sta nel core, nella funzione check_read_permission del controller REST dei post. Un allegato ha stato inherit: se è collegato a un articolo, eredita la visibilità dell’articolo. Se non è collegato a nulla, il commento nel codice dice testualmente che WordPress lo considera pubblicato.
Quindi un’immagine caricata dentro una bozza sparisce dall’elenco, ma un file caricato direttamente nella Libreria media, da un plugin o da uno script, compare nell’elenco pubblico. Sul mio sito sono il 94% del totale.
3. Le pagine allegato
Fino a WordPress 6.4 ogni file caricato aveva una sua pagina HTML, indicizzabile. Dalla 6.4 le pagine allegato sono disattivate sulle nuove installazioni e reindirizzano al file. Sui siti aggiornati da versioni precedenti, invece, l’opzione wp_attachment_pages_enabled resta a 1 e le pagine continuano a esistere. Il mio sito è nato nel 2020, e infatti /?attachment_id= risponde ancora 200.
Il caso del mio sito: la copertina esce prima dell’articolo
Il dato che mi ha sorpreso di più non riguarda i file vecchi. Riguarda il modo in cui pubblico oggi.
La pipeline che uso per questo blog carica prima l’immagine in evidenza, poi crea la bozza e le assegna l’immagine con il campo featured_media. Assegnare un’immagine in evidenza non la collega all’articolo come allegato: il campo post dell’immagine resta vuoto. Risultato: per tutto il tempo in cui l’articolo è in bozza, in revisione o fermo per un controllo fallito, la sua copertina è già nell’elenco pubblico dei media. E il nome del file è lo slug dell’articolo.
Per un blog tecnico è un fastidio, non un incidente. Per un’agenzia che prepara il sito di un cliente prima del lancio, per un’azienda che carica la scheda di un prodotto non ancora annunciato o per uno studio che mette online i materiali di un bando, lo stesso comportamento anticipa informazioni che dovevano restare riservate fino a una data precisa. Non serve nessuna vulnerabilità: basta una chiamata GET documentata.
La correzione lato pipeline è banale e la applico da oggi: passare l’ID della bozza nel caricamento, così il file eredita lo stato dell’articolo e sparisce dall’elenco finché l’articolo non è pubblicato. L’URL del file resta raggiungibile, ma almeno non viene servito a chi chiede l’elenco.
I form con allegato: dove finiscono i file dei clienti
La parte più delicata non sono le immagini degli articoli. Sono i file che i visitatori ti mandano: curriculum, documenti d’identità, preventivi, planimetrie, referti. Ogni plugin di form li tratta in modo diverso.
Contact Form 7
È il comportamento più prudente fra i tre. Il file viene spostato in una cartella temporanea, wp-content/uploads/wpcf7_uploads, allegato all’email e poi rimosso dalla cartella. Sul server non resta nulla. Il file vive nella casella di posta di chi riceve la notifica, e lì valgono le regole della posta.
Gravity Forms
I file restano sul server, in sottocartelle di uploads/gravity_forms/ con nomi generati da un HMAC salato, e ogni cartella contiene un index.html vuoto per impedire l’elenco del contenuto su server configurati male (documentazione). I link di download passano da un URL mascherato che non rivela il percorso reale. Come già detto, però, quell’URL non chiede chi sei, e un’entry spostata nel cestino o marcata come spam conserva il file: lo cancella solo l’eliminazione definitiva.
WPForms
Per impostazione predefinita i file finiscono nella cartella wpforms dentro gli upload del sito. C’è un’opzione per salvarli nella Libreria media di WordPress (guida al campo File Upload): comoda per ritrovarli, ma se la attivi i documenti dei clienti diventano allegati come le immagini del blog. Verifica con la chiamata REST del paragrafo successivo se compaiono nell’elenco pubblico: su un sito con form di candidatura è il primo controllo che farei.
La mia lettura: il plugin che preferisco è quello che i file non li tiene. Un documento che non è sul server non può uscire dal server.
Audit in 15 minuti, con quattro richieste
Non serve accesso all’amministrazione. Sostituisci tuosito.it con il tuo dominio e lancia queste richieste da un terminale. Sono le stesse che ho usato per i numeri in apertura.
# 1. Quanti file conta l'elenco pubblico
curl -s -D - -o /dev/null "https://tuosito.it/wp-json/wp/v2/media?per_page=1" | grep -i x-wp-total
# 2. Quanti non sono collegati a nessun articolo
curl -s -D - -o /dev/null "https://tuosito.it/wp-json/wp/v2/media?parent=0&per_page=1" | grep -i x-wp-total
# 3. Quali documenti ci sono (PDF, poi ripeti con altri tipi)
curl -s "https://tuosito.it/wp-json/wp/v2/media?mime_type=application/pdf&per_page=100&_fields=date,source_url"
# 4. Le cartelle mostrano il contenuto? Deve rispondere 403 o 404, non 200
curl -s -o /dev/null -w "%{http_code}\n" "https://tuosito.it/wp-content/uploads/"Come leggere i risultati. Il totale della prima richiesta ti dice quanto è grande la superficie. La seconda ti dice quanta parte non è legata a un contenuto pubblicato, cioè quanti file nessuno ha deciso di mostrare. La terza è quella che conta davvero: guarda i nomi. Un preventivo-rossi-2024.pdf o un carta-identita.jpg in quell’elenco sono un problema di protezione dei dati, non di SEO. La quarta controlla l’elenco delle cartelle, che sul mio hosting è già bloccato con un 403.
Aggiungi due controlli manuali. Apri /?attachment_id= seguito da un ID preso dall’elenco: se vedi una pagina e non il file, le pagine allegato sono ancora attive. E se usi Gravity Forms o WPForms, entra via FTP o dal file manager dell’hosting e guarda quanti mesi di upload contengono le loro cartelle. È il numero che dice quanto hai da perdere.
Chiudere l’elenco pubblico dei media
Un visitatore non ha bisogno dell’elenco dei tuoi file: le immagini degli articoli arrivano nell’HTML della pagina, non dall’API. Chi ne ha bisogno è l’editor a blocchi, che però lavora da utente autenticato. Quindi si può limitare l’endpoint agli utenti che hanno il permesso di caricare file.
<?php
/**
* Limita l'endpoint REST dei media agli utenti che possono caricare file.
* L'editor continua a funzionare (lavora autenticato), i visitatori
* anonimi ricevono un 401 invece dell'elenco completo degli upload.
* stripos e non strpos: WordPress risolve le rotte senza distinguere
* maiuscole e minuscole, quindi /wp/v2/MEDIA porta allo stesso elenco.
*/
add_filter(
'rest_pre_dispatch',
function ( $result, $server, $request ) {
if ( 0 === stripos( $request->get_route(), '/wp/v2/media' ) && ! current_user_can( 'upload_files' ) ) {
return new WP_Error(
'rest_forbidden',
esc_html__( 'Accesso non consentito.', 'default' ),
array( 'status' => rest_authorization_required_code() )
);
}
return $result;
},
10,
3
);<?php.Due avvertenze prima di incollarlo. Se il sito ha un frontend headless o un’app che legge le immagini con _embed senza autenticarsi, quelle chiamate smettono di restituire l’immagine incorporata: verifica prima. Anche i ruoli senza upload_files, come il Collaboratore, perdono l’accesso all’endpoint: se lavorano nell’editor con immagini già caricate, provalo con il loro account. E questo snippet chiude l’elenco, non i file. Chi ha già l’URL di un documento continua ad aprirlo.
Per le pagine allegato su un sito nato prima della 6.4 basta portare l’opzione a zero, per esempio con WP-CLI:
wp option update wp_attachment_pages_enabled 0Su Gravity Forms c’è un filtro documentato che fa quello che molti credono succeda già: richiede il login prima di servire un download (elenco dei filtri di download).
<?php
// Gravity Forms: i link di download funzionano solo per utenti autenticati.
add_filter( 'gform_require_login_pre_download', '__return_true' );Attenzione: basta un account qualsiasi. Su un sito con clienti registrati (WooCommerce, area riservata) un utente iscritto scarica comunque i file, e il filtro non copre chi apre il percorso diretto nella cartella gravity_forms. In quel caso serve anche un controllo sul ruolo.
Chiudere i file: cosa protegge davvero e cosa no
Qui si fanno gli errori più comuni, perché ci sono strumenti che sembrano di sicurezza e servono ad altro.
Il noindex non protegge. L’intestazione X-Robots-Tag dice ai motori di ricerca di non mostrare un file nei risultati, e Google la documenta anche per i PDF. È utile per evitare che un listino vecchio compaia su Google. Non ferma nessuno che abbia il link. Se vuoi comunque applicarla ai documenti su un server Apache o LiteSpeed:
# .htaccess in wp-content/uploads/
<IfModule mod_headers.c>
<FilesMatch "(?i)\.(pdf|docx?|xlsx?|zip)$">
Header set X-Robots-Tag "noindex, nofollow"
</FilesMatch>
</IfModule>Il nome casuale non protegge. Rallenta chi va per tentativi, niente di più. È la stessa illusione dei link non elencati di OpenAI.
Cosa protegge davvero, in ordine di efficacia:
- Non tenere il file. Se la notifica email basta al tuo processo, fai come Contact Form 7: allega e cancella. Se il file serve, stabilisci per quanti giorni e cancella le entry in modo definitivo, non nel cestino.
- Portarlo fuori dal server web. Uno spazio cloud con permessi per utente, collegato al form, sposta il problema su un sistema progettato per gestire gli accessi.
- Negare l’accesso diretto e servire il file da PHP. La cartella risponde 403 a tutti, e un piccolo script verifica il permesso dell’utente prima di inviare il file. È la sola soluzione che resta sul server e controlla davvero chi scarica. Richiede lavoro e test, perché i link nelle notifiche email smettono di funzionare per chi non è loggato.
Per i dati di clienti o candidati conservare meno è anche la posizione più semplice da difendere: lo chiedono il principio di minimizzazione del GDPR e quello di proporzionalità della legge svizzera sulla protezione dei dati. E un file cancellato non richiede policy di accesso.
Agenti AI con i permessi di caricamento
Torno al caso OpenAI, perché qui diventa operativo. Sempre più siti danno a un agente o a un’automazione una Application Password con ruolo Autore o Editor: per pubblicare bozze, caricare immagini, aggiornare schede prodotto. Ogni file che quell’agente carica via REST senza indicare un articolo di destinazione diventa, nell’istante del caricamento, un file pubblico ed elencato.
Un agente non distingue un’immagine di copertina dalla scansione di un documento che gli hai passato come contesto. Se il suo strumento di lavoro è «carica su WordPress e usa l’URL», carica. Gli agenti di OpenAI hanno fatto esattamente questo con un servizio di image hosting: hanno usato un posto pubblico per default come se fosse un deposito di lavoro.
Tre regole che applico ai siti su cui lavorano automazioni:
- Un utente dedicato per ogni agente, con il ruolo minimo, e una password applicativa revocabile da sola. Ho descritto la procedura nella guida su come revocare gli accessi REST a un esterno.
- Caricamento sempre legato a un articolo, con il parametro
postnella richiesta. Così il file segue lo stato della bozza invece di essere pubblico dal primo secondo. - I documenti dei clienti non passano mai dalla Libreria media. Se un agente deve leggerli, li legge da dove stanno, con un accesso in sola lettura.
Chi usa già agenti su dati sensibili trova il resto del discorso, log ed escalation, nell’articolo sugli incidenti degli agenti AI. Qui il punto è più semplice e più vecchio degli agenti: WordPress, nel dubbio, sceglie «pubblico». Se non decidi tu dove finisce un file, lo ha già deciso lui.
Chiusura operativa
Se hai un quarto d’ora lunedì mattina, fai questo nell’ordine:
- Lancia le quattro richieste dell’audit e salva i numeri.
- Scorri l’elenco dei PDF e dei documenti: tutto quello che non dovrebbe essere online va cancellato dalla Libreria media, non solo scollegato.
- Se usi form con allegati, controlla da quanti mesi i file si accumulano e decidi una durata di conservazione.
- Applica lo snippet che limita
/wp/v2/media, dopo aver verificato che nessun frontend lo usi in anonimo. - Su siti nati prima di WordPress 6.4, disattiva le pagine allegato.
Il mio sito, oggi, è al punto uno: 1.056 file nell’elenco pubblico, un PDF da rivedere e una pipeline corretta da oggi. Un link difficile da indovinare non protegge un file: lo protegge solo un server che chiede chi sei.
Fonti
- TechCrunch, 25 settembre 2026: gli agenti OpenAI e le 53 immagini degli utenti
- WordPress core: class-wp-rest-posts-controller.php, check_read_permission
- REST API Handbook: endpoint Media
- Make WordPress Core: Changes to attachment pages (WordPress 6.4)
- Contact Form 7: File uploading and attachment
- Gravity Forms: File Upload Security
- WPForms: guida al campo File Upload
- Google Search Central: specifiche di robots meta e X-Robots-Tag

