
Aviso técnico e prova de conceito para CVE-2026-97286, um XSS armazenado autenticado no plugin Strong Testimonials do WordPress, com cadeia de ataque, PoC e análise de patch.
Um Contributor autenticado pode armazenar metadados de foto de plataforma controlados pelo atacante no plugin Strong Testimonials do WordPress. Antes da versão 3.3.12, o plugin renderizava o valor armazenado dentro de um atributo src de imagem sem escape contextual. Depois que um administrador publica o depoimento elaborado e um post que o exibe, o payload é executado para os visitantes que abrem esse post, incluindo visitantes não autenticados.
Esta pesquisa foi realizada em um laboratório local autorizado e divulgada através do Patchstack antes do lançamento público.
| Campo | Valor |
|---|---|
| CVE | CVE-2026-97286 |
| Produto | Strong Testimonials |
| Fornecedor | WP Chill |
| Slug do plugin | strong-testimonials |
| Vulnerabilidade | Cross-site scripting armazenado autenticado |
| Versões afetadas | <= 3.3.11 |
| Versão corrigida | 3.3.12 |
| Privilégio necessário | Contributor ou superior |
| CWE | CWE-79: Neutralização Imprópria de Entrada Durante a Geração de Página Web |
| CVSS v3.1 | 6.5 Médio — CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:L |
| Pesquisador | Rully Miftahur Rozaq |
| Reportado | 16 de setembro de 2026 |
| Divulgação pública | 30 de setembro de 2026 |
O Patchstack classifica o advisory como de baixa prioridade, embora a severidade base CVSS v3.1 atribuída seja Médio.
O editor de depoimentos aceita um array de requisição custom e persiste cada chave enviada como metadado do post. Chaves não presentes na definição de campo personalizado configurada passam por sanitize_text_field(). Isso permite que um Contributor envie metadados reservados pelo plugin, como platform e platform_user_photo, ao editar um depoimento que lhe pertence.
Para plataformas sociais suportadas, o plugin recupera platform_user_photo, verifica se a URL retorna uma resposta HTTP 200 e, em seguida, interpola a string armazenada original em um atributo <img src="https://raw.githubusercontent.com/rully2212/cve-2026-97286/main/...">. A validação do status HTTP não torna uma string segura para um atributo HTML. Uma aspa no fragmento da URL pode encerrar o src e introduzir um manipulador de eventos.
A cadeia de ataque verificada é:
A injeção é autenticada. O visitante que aciona o payload já publicado não precisa estar autenticado.
A exploração bem-sucedida proporciona execução persistente de JavaScript na origem WordPress afetada em páginas que renderizam o depoimento elaborado através do caminho vulnerável da miniatura. O código é executado com os privilégios do navegador e a sessão WordPress do visitante, se houver.
O caminho reproduzido requer publicação por um administrador, o que justifica a métrica CVSS UI:R. Esta pesquisa não demonstrou injeção não autenticada, execução automática em todas as páginas do site, escalada de privilégios, roubo de cookies ou comprometimento total do site.
A prova de conceito foi reproduzida com a seguinte configuração:
| Componente | Configuração |
|---|---|
| WordPress | 7.1 |
| Strong Testimonials | 3.3.7, edição gratuita |
| Tema | Twenty Twenty-Five |
| Template de exibição | default-theme |
| Miniatura | Ativada |
| PHP / Apache | PHP 8.3.33 / Apache 2.4.68 |
| Banco de dados | MariaDB 11.4 |
| Função do atacante | Contributor |
| Capacidades do atacante | edit_posts=true, publish_posts=false, unfiltered_html=false |
A versão 3.3.7 foi a versão exata usada no laboratório. O registro público do CVE identifica todas as versões até 3.3.11 como afetadas e a versão 3.3.12 como corrigida.
Em admin/class-strong-testimonials-post-editor.php, save_details() itera sobre cada entrada no array custom enviado. Uma chave não reconhecida chega ao ramo genérico de sanitização e é então armazenada com 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 );
}
O handler real realiza verificações de nonce e sanitização específica por campo. A descoberta não depende de contornar essas verificações nem de editar o depoimento de outro usuário. O Contributor edita legitimamente o seu próprio depoimento, mas fornece as chaves reservadas platform e platform_user_photo.
Em versões vulneráveis, includes/functions-image.php lê platform_user_photo, realiza uma verificação do status HTTP através de wpmtst_is_valid_image_url() e insere a string original num 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}'" : ''
);
A porção de fragmento de uma URL não é enviada ao servidor HTTP, mas permanece na string posteriormente renderizada pelo PHP. O servidor pode, portanto, ver uma URL inofensiva retornando HTTP 200 enquanto o navegador recebe um valor contendo uma sequência de quebra de atributo.
O template default-theme chama wpmtst_get_thumbnail() e emite diretamente a marcação de imagem retornada:
<figure class="wp-block-image wpmtst-testimonial-avatar">
<?php echo $wpmtst_avatar_html; ?>
</figure>
O template pressupõe que o helper já tenha produzido HTML seguro. Nas versões afetadas, o helper de foto da plataforma não escapava a URL para o seu contexto src.
Reproduza este problema apenas num sistema que lhe pertença ou que esteja explicitamente autorizado a testar.
<= 3.3.11 num site WordPress de teste autorizado.default-theme com miniaturas ativadas.Inicie sessão como Contributor e crie um novo rascunho de testemunho.
Capture o pedido legítimo de gravação POST /wp-admin/post.php usando um proxy de interceção. Mantenha o corpo completo do formulário, o cookie de sessão, um nonce WordPress recente, o nonce do plugin e o ID do testemunho pertencente ao Contributor.
Acrescente estes parâmetros ao corpo do formulário:
custom[platform]=facebook
custom[platform_user_photo]=http://wordpress/index.php?rest_route=/#" onerror="alert(1)" x="
fragmento de formulário codificado em 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
O hostname wordpress é o nome do serviço Docker usado no laboratório local. Em outro ambiente autorizado, substitua-o por um endpoint de teste controlado pelo pesquisador que seja acessível pelo WordPress e retorne HTTP 200. Não direcione a serviços internos de terceiros ou sensíveis.