
Aviso técnico y prueba de concepto para CVE-2026-97286, un XSS almacenado autenticado en el plugin Strong Testimonials de WordPress, con cadena de ataque, PoC y análisis del parche.
Un Colaborador autenticado puede almacenar metadatos de foto de plataforma controlados por el atacante en el plugin de WordPress Strong Testimonials. Antes de la versión 3.3.12, el plugin renderizaba el valor almacenado dentro de un atributo src de imagen sin escape contextual. Después de que un administrador publique el testimonio manipulado y una entrada que lo muestre, la carga útil se ejecuta para los visitantes que abran esa entrada, incluidos los visitantes no autenticados.
Esta investigación se realizó en un laboratorio local autorizado y se divulgó a través de Patchstack antes de su publicación pública.
| Campo | Valor |
|---|---|
| CVE | CVE-2026-97286 |
| Producto | Strong Testimonials |
| Proveedor | WP Chill |
| Slug del plugin | strong-testimonials |
| Vulnerabilidad | Cross-site scripting almacenado autenticado |
| Versiones afectadas | <= 3.3.11 |
| Versión parcheada | 3.3.12 |
| Privilegio requerido | Colaborador o superior |
| CWE | CWE-79: Neutralización incorrecta de la entrada durante la generación de páginas web |
| CVSS v3.1 | 6.5 Medio — CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:L |
| Investigador | Rully Miftahur Rozaq |
| Reportado | 16 de septiembre de 2026 |
| Divulgación pública | 30 de septiembre de 2026 |
Patchstack clasifica el aviso como de baja prioridad aunque la severidad base asignada según CVSS v3.1 sea Media.
El editor de testimonios acepta un array de solicitud custom y persiste cada clave enviada como metadatos de la entrada. Las claves que no están presentes en la definición de campo personalizado configurada pasan por sanitize_text_field(). Esto permite a un Colaborador enviar metadatos reservados por el plugin, como platform y platform_user_photo, mientras edita un testimonio del que es propietario.
Para las plataformas sociales compatibles, el plugin recupera platform_user_photo, verifica que la URL devuelva una respuesta HTTP 200 y luego interpola la cadena original almacenada dentro de un atributo <img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/...">. La validación del estado HTTP no hace que una cadena sea segura para un atributo HTML. Una comilla en el fragmento de la URL puede terminar el src e introducir un controlador de eventos.
La cadena de ataque verificada es:
La inyección es autenticada. El visitante que desencadena la carga útil ya publicada no necesita estar autenticado.
La explotación exitosa proporciona ejecución persistente de JavaScript en el origen de WordPress afectado en las páginas que renderizan el testimonio manipulado a través de la ruta vulnerable de la miniatura. El código se ejecuta con los privilegios del navegador y la sesión de WordPress del visitante, si la hubiera.
La ruta reproducida requiere la publicación por parte de un administrador, lo que explica la métrica UI:R de CVSS. Esta investigación no demostró inyección no autenticada, ejecución automática en todas las páginas del sitio, escalada de privilegios, robo de cookies ni compromiso total del sitio.
La prueba de concepto se reprodujo con la siguiente configuración:
| Componente | Configuración |
|---|---|
| WordPress | 7.1 |
| Strong Testimonials | 3.3.7, edición gratuita |
| Tema | Twenty Twenty-Five |
| Plantilla de visualización | default-theme |
| Miniatura | Habilitada |
| PHP / Apache | PHP 8.3.33 / Apache 2.4.68 |
| Base de datos | MariaDB 11.4 |
| Rol del atacante | Colaborador |
| Capacidades del atacante | edit_posts=true, publish_posts=false, unfiltered_html=false |
La versión 3.3.7 fue la versión exacta utilizada en el laboratorio. El registro público de CVE identifica todas las versiones hasta la 3.3.11 como afectadas y la versión 3.3.12 como corregida.
En admin/class-strong-testimonials-post-editor.php, save_details() itera sobre cada entrada en el array custom enviado. Una clave no reconocida llega a la rama de saneamiento genérica y luego se almacena con update_post_meta():
$custom = $_POST['custom'];
foreach ( $custom as $key => $value ) {
// ...
$sanitized_value = sanitize_text_field( $value );
update_post_meta( $post_id, $key, $sanitized_value );
}
El manejador real realiza comprobaciones de nonce y saneamiento específico por campo. El hallazgo no depende de eludir esas comprobaciones ni de editar el testimonio de otro usuario. El Colaborador edita legítimamente su propio testimonio, pero proporciona las claves reservadas platform y platform_user_photo.
En las versiones vulnerables, includes/functions-image.php lee platform_user_photo, realiza una comprobación del estado HTTP mediante wpmtst_is_valid_image_url() e inserta la cadena original en un atributo HTML:
$platform_user_photo = get_post_meta( $post_id, 'platform_user_photo', true );
if ( ! $platform_user_photo || ! wpmtst_is_valid_image_url( $platform_user_photo ) ) {
return $img;
}
return sprintf(
'<img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/%25s" %s %s/>',
$platform_user_photo,
$width ? "width='{$width}'" : '',
$height ? "height='{$height}'" : ''
);
La porción de fragmento de una URL no se envía al servidor HTTP, pero permanece en la cadena que posteriormente se renderiza mediante PHP. Por lo tanto, el servidor puede ver una URL inofensiva que devuelve HTTP 200 mientras el navegador recibe un valor que contiene una secuencia de escape de atributo.
La plantilla default-theme llama a wpmtst_get_thumbnail() y emite directamente el marcado de imagen devuelto:
<figure class="wp-block-image wpmtst-testimonial-avatar">
<?php echo $wpmtst_avatar_html; ?>
</figure>
La plantilla asume que el helper ya ha producido HTML seguro. En las versiones afectadas, el helper de fotos de la plataforma no escapaba la URL para su contexto src.
Reproduce este problema únicamente en un sistema que poseas o para el que tengas autorización explícita de prueba.
<= 3.3.11 en un sitio de prueba de WordPress autorizado.default-theme con miniaturas habilitadas.Inicia sesión como Contributor y crea un nuevo borrador de testimonial.
Captura la solicitud legítima de guardado POST /wp-admin/post.php usando un proxy de interceptación. Conserva su cuerpo de formulario completo, la cookie de sesión, un nonce de WordPress reciente, el nonce del plugin y el ID del testimonial propiedad del Contributor.
Añade estos parámetros al cuerpo del formulario:
custom[platform]=facebook
custom[platform_user_photo]=http://wordpress/index.php?rest_route=/#" onerror="alert(1)" x="
Fragmento de formulario codificado en URL:
&custom%5Bplatform%5D=facebook&custom%5Bplatform_user_photo%5D=http%3A%2F%2Fwordpress%2Findex.php%3Frest_route%3D%2F%23%22%20onerror%3D%22alert%281%29%22%20x%3D%22