Indice dei contenuti dell'articolo:
Introduzione
Pixel Your Site (PYS) è uno dei plugin WordPress più diffusi per la gestione dei pixel di tracciamento e delle integrazioni con le principali piattaforme di advertising e analytics. Viene utilizzato per configurare e gestire facilmente strumenti come Facebook Pixel, Google Analytics, TikTok Pixel, Google Ads e altre piattaforme di marketing, senza richiedere modifiche manuali al codice del sito. Grazie alla sua semplicità di configurazione e alla capacità di centralizzare diversi sistemi di tracking, il plugin ha raggiunto una diffusione significativa nell’ecosistema WordPress, con oltre 500.000 installazioni attive e una presenza capillare soprattutto su siti eCommerce e progetti orientati al marketing digitale.
Proprio questa ampia diffusione rende particolarmente rilevante qualsiasi problema di sicurezza presente nel codice del plugin. Durante un’analisi tecnica approfondita del suo funzionamento interno, è emersa una vulnerabilità che consente a un attaccante di iniettare domini o stringhe di testo arbitrarie all’interno dell’HTML generato dal sito. L’aspetto più critico è che questo comportamento può essere sfruttato da chiunque, senza alcuna autenticazione o accesso privilegiato al sito target. In altre parole, non è necessario essere amministratori, utenti registrati o avere qualsiasi tipo di accesso alla piattaforma WordPress per sfruttare il problema.
La situazione diventa ancora più problematica quando il sito utilizza un sistema di caching, una configurazione estremamente comune nei siti WordPress in produzione. Plugin di cache come WP Super Cache, W3 Total Cache, WP Rocket o LiteSpeed Cache — oppure sistemi di caching a livello CDN come Cloudflare — memorizzano le pagine HTML generate dal server per ridurre il carico e migliorare le prestazioni. In presenza della vulnerabilità, questo meccanismo può trasformare un input controllato dall’attaccante, normalmente limitato alla singola richiesta HTTP, in un contenuto persistente che verrà servito a tutti i visitatori successivi fino alla scadenza della cache. Di fatto, una singola richiesta malevola può “avvelenare” la versione cached di una pagina, rendendo l’iniezione visibile a migliaia di utenti e anche ai crawler dei motori di ricerca.
Come ha evidenziato Michele Genito, noto esperto WordPress italiano che ha portato alla luce il problema e ne ha divulgato i dettagli tecnici, la vulnerabilità presenta implicazioni ben più serie di quanto possa sembrare a prima vista. Nonostante la segnalazione sia stata inoltrata ai canali di sicurezza competenti, il team di Wordfence ha rigettato la segnalazione classificandola come non rilevante dal punto di vista della sicurezza. Secondo questa valutazione, il comportamento osservato sarebbe riconducibile al fatto che il campo Referer è intrinsecamente controllabile dal client e quindi non rappresenterebbe di per sé una vulnerabilità.
Questa nostra analisi tecnica dimostra invece perché tale interpretazione risulti, a nostro avviso, incompleta e superficiale. Il problema non risiede semplicemente nella possibilità di manipolare il referrer — cosa effettivamente nota da sempre nel contesto HTTP — ma nella combinazione di diversi fattori architetturali: l’utilizzo diretto di input non fidati, la loro esposizione nel markup HTML servito a tutti gli utenti tramite JavaScript inline e, soprattutto, l’effetto moltiplicatore introdotto dai sistemi di caching. È proprio questa combinazione che trasforma un dato temporaneo per-request in un contenuto persistente e potenzialmente sfruttabile in scenari di cache poisoning e SEO manipulation.
Il meccanismo: come PYS espone il TrafficSource nel DOM
Il plugin genera una variabile JavaScript globale pysOptions tramite wp_localize_script(), che viene renderizzata inline nel sorgente HTML di ogni pagina:
<script id="pys-js-extra">
var pysOptions = {
...
"tracking_analytics": {
"TrafficSource": "google.com",
"TrafficLanding": "https://esempio.it/pagina/",
"TrafficUtms": { ... },
"TrafficUtmsId": { ... }
},
...
};
</script>
Il campo TrafficSource contiene il dominio di provenienza del visitatore. Questo valore viene determinato server-side dalla funzione getTrafficSource() definita in includes/functions-common.php e inserito nell’array di opzioni in includes/class-events-manager.php alla riga 188:
$options['tracking_analytics'] = array(
"TrafficSource" => getTrafficSource(),
"TrafficLanding" => sanitize_url($_COOKIE['pys_landing_page'] ?? $_SESSION['LandingPage'] ?? 'undefined'),
"TrafficUtms" => getUtms(),
"TrafficUtmsId" => getUtmsId(),
);
L’array viene poi serializzato nel DOM:
wp_localize_script('pys', 'pysOptions', $data);
Analisi della funzione vulnerabile
Ecco la funzione getTrafficSource() nella sua interezza:
function getTrafficSource () {
$referrer = "";
$source = "";
try {
if (isset($_SERVER['HTTP_REFERER'])) {
$referrer = $_SERVER['HTTP_REFERER']; // [1] Input non sanitizzato
}
$direct = empty($referrer);
$internal = $direct ? false : (substr($referrer, 0, strlen(site_url())) === site_url());
$external = !$direct && !$internal;
$cookie = sanitize_text_field(
!isset($_COOKIE['pysTrafficSource']) ? null : $_COOKIE['pysTrafficSource']
); // [2] Cookie controllabile dall'utente
$session = sanitize_text_field(
!isset($_SESSION['TrafficSource']) ? null : $_SESSION['TrafficSource']
);
if (!$external) {
$source = $cookie || $session ? $cookie ?? $session : 'direct';
} else {
$source = ($cookie && $cookie === $referrer)
|| ($session && $session === $referrer)
? $cookie ?? $session
: $referrer; // [3] Referrer usato direttamente
}
if ($source !== 'direct') {
$parse = parse_url($source);
if (isset($parse['host'])) {
return $parse['host']; // [4] Protezione parziale
} elseif ($source == $cookie || $source == $session) {
return $source; // [5] BYPASS: valore restituito senza parse_url
} else {
return defined('REST_REQUEST') && REST_REQUEST ? 'REST API' : "direct";
}
} else {
return defined('REST_REQUEST') && REST_REQUEST ? 'REST API' : $source;
}
} catch (\Exception $e) {
return "direct";
}
}
I punti critici
- [1] $_SERVER[‘HTTP_REFERER’] non sanitizzato. L’header HTTP Referer è interamente controllabile dal client. Qualsiasi valore può essere inviato con una semplice richiesta curl.
- [2] Cookie pysTrafficSource controllabile. Il cookie è impostato lato client dal JavaScript del plugin stesso, quindi un attaccante può impostarlo con qualsiasi valore.
sanitize_text_field()rimuove i tag HTML ma non impedisce l’inserimento di domini arbitrari o testo non-HTML. - [3] Referrer usato come source. Quando il referrer è esterno e non corrisponde ai valori in cookie/sessione, viene usato direttamente come
$source. - [4] parse_url() come protezione parziale. Il codice tenta di estrarre solo l’host dal valore, ma questa protezione è facilmente aggirabile.
- [5] Il bypass critico. Quando
$sourcecorrisponde al cookie o alla sessione, il valore viene restituito integralmente senza passare perparse_url(). Questo significa che un valore arbitrario memorizzato nel cookiepysTrafficSourcepuò finire direttamente nell’HTML.
L’attacco: Cache Poisoning + SEO Poisoning
Cache Poisoning + SEO Poisoning è una tecnica di attacco che sfrutta i sistemi di caching dei siti web per inserire contenuti malevoli o manipolati all’interno delle pagine memorizzate in cache. Un attaccante invia una richiesta appositamente costruita che altera l’output HTML generato dal server; se la pagina viene salvata nella cache in quel momento, la versione manipolata verrà servita a tutti i visitatori successivi. Quando il contenuto iniettato include domini o riferimenti esterni indesiderati, l’attacco può trasformarsi in SEO poisoning, influenzando l’interpretazione della pagina da parte dei motori di ricerca e associando il sito vittima a domini spam, malware o contenuti indesiderati.
Prerequisiti
- Il sito target utilizza Pixel Your Site (qualsiasi versione attuale)
- Il sito ha un sistema di cache attivo (WP Super Cache, W3 Total Cache, WP Rocket, LiteSpeed Cache, Cloudflare Page Cache, Varnish Cache ecc.)
- La pagina target non è ancora in cache (o la cache è appena scaduta)
Scenario di attacco base
# L'attaccante visita la pagina nel momento in cui la cache viene rigenerata
curl -s -H "Referer: https://sito-pornografico.xxx" \
https://sitovittima.it/pagina-importante/ > /dev/null
Da questo momento, la risposta HTML viene memorizzata nella cache con il TrafficSource impostato al dominio dell’attaccante. Tutti i visitatori successivi — inclusi i crawler dei motori di ricerca — vedranno:
var pysOptions = {
"tracking_analytics": {
"TrafficSource": "sito-pornografico.xxx",
...
}
};
Scenario avanzato: cookie injection
# L'attaccante imposta il cookie con testo arbitrario
curl -s -b "pysTrafficSource=testo-offensivo-qualsiasi" \
-H "Referer: testo-offensivo-qualsiasi" \
https://sitovittima.it/ > /dev/null
Poiché il referrer corrisponde al cookie, il codice entra nel branch [5] e restituisce il valore senza validazione del formato dominio. sanitize_text_field() rimuove i tag HTML, ma qualsiasi stringa di testo non-HTML passa indisturbata.
Impatti concreti
- Negative SEO. Un competitor o un attaccante può associare domini in blacklist (pornografia, malware, gambling) al sito vittima. I crawler dei motori di ricerca analizzano il contenuto completo della pagina, incluso il JavaScript inline.
- Competitor sabotage. Inserire il dominio di un competitor diretto crea false associazioni, potenzialmente interpretabili dai motori di ricerca come relazioni tra i due siti.
- Defacement soft. Pur non essendo un XSS (perché
wp_localize_scriptesegue escaping dei valori), il contenuto è visibile nel sorgente HTML. - Persistenza. L’attacco persiste per tutta la durata della cache. Con configurazioni comuni (cache di 12–24 ore), una singola richiesta malevola può avvelenare la pagina per un’intera giornata.
Perché non è “solo un referrer”
La risposta tipica a questa classe di problemi è: “il referrer è sempre controllabile, non è una vulnerabilità”. Questa affermazione è corretta in isolamento, ma ignora il contesto.
- Il referrer viene persistito nell’output HTML.
- La cache amplifica l’impatto.
- Non serve autenticazione.
- Il costo dell’attacco è zero.
Fix proposto
if ($source !== 'direct') {
$parse = parse_url($source);
if (isset($parse['host'])) {
$host = $parse['host'];
} elseif ($source == $cookie || $source == $session) {
$parse_stored = parse_url($source);
$host = isset($parse_stored['host']) ? $parse_stored['host'] : $source;
} else {
return defined('REST_REQUEST') && REST_REQUEST ? 'REST API' : "direct";
}
$host = preg_replace('/[^a-zA-Z0-9.\-]/', '', $host);
if (empty($host) || !preg_match(
'/^([a-zA-Z0-9]([a-zA-Z0-9\-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/',
$host
)) {
return "direct";
}
return $host;
}
Cosa cambia
- Eliminazione del bypass. Anche i valori provenienti da cookie e sessione vengono processati tramite
parse_url(). - Sanitizzazione dei caratteri. Un
preg_replacerimuove qualsiasi carattere non valido per un nome di dominio. - Validazione del formato. Una regex verifica che il risultato sia un dominio sintatticamente valido.
Conclusioni
La vulnerabilità risiede nella combinazione di tre fattori: input non fidato (Referer e cookie) persistito nell’output HTML tramite wp_localize_script(), assenza di validazione del formato e amplificazione tramite cache che rende l’iniezione persistente.
Non si tratta di un XSS tradizionale, ma classificare una vulnerabilità esclusivamente in base alla tassonomia OWASP classica significa ignorare vettori di attacco reali. Il cache poisoning combinato con SEO poisoning è un rischio concreto, documentato, e in questo caso trivialmente sfruttabile.
Un plugin installato su centinaia di migliaia di siti dovrebbe trattare qualsiasi dato proveniente dal client come potenzialmente malevolo — specialmente quando quel dato finisce nel markup servito a tutti i visitatori.