CVE-2026-13726 CVSS 7.1 · Alto
Software: Multiple Page Generator Plugin – MPG (plugin WordPress) · slug multiple-pages-generator-by-porthas
Versões afetadas: < 4.1.8 (testado no 4.1.7 — latest na data) · Active installs: ~2.000
Classe: Reflected XSS não autenticado (CWE-79) · OWASP A03:2021 — Injection
Vetor: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L
Privilégio necessário: nenhum (não autenticado) · Crédito: Equipe TreeSec
Data descoberta: 29 de junho de 2026 · Submetido ao WPScan em 29/06/2026
Referências: WPScan
Aviso legal: toda a validação foi feita exclusivamente em laboratório próprio (WordPress + PHP 8.2 em Docker), de forma não destrutiva. Nenhum sistema de terceiros foi testado ou afetado. O script completo de automação está sob divulgação coordenada e não está incluído neste artigo.
Neste write-up
1. Por que um XSS não autenticado importa
A narrativa comum sobre XSS é: “precisa de interação do usuário, não é crítico.” A realidade é que XSS não autenticado é uma das classes mais úteis para atacantes em campanha: não exige conta, não deixa rastro de login, e o payload pode ser entregue por qualquer canal — e-mail, mensagem, notificação push, QR code.
Quando a vítima é um administrador com sessão ativa, o ataque escala para sequestro de painel: criação de usuários admin, instalação de plugins maliciosos, exfiltração de dados. Tudo isso com um único link.
O que torna este achado interessante além da classe é a combinação de duas falhas independentes: um nonce que deveria ser privado vazando para qualquer visitante, e um echo sem encode que age como sink. Nenhuma das duas, isolada, é suficiente para o ataque — juntas, formam um vetor não autenticado completo.
2. A superfície: handler nopriv + nonce que vaza
O plugin Multiple Page Generator cria páginas em massa a partir de templates e datasets. Para renderizar pré-visualizações de shortcode no front-end, ele expõe um handler AJAX registrado para usuários não autenticados:
// HookController.php:423
add_action('wp_ajax_nopriv_mpg_shortcode', [$this, 'shortcodeAjaxHandler']); // ← NÃO comentado
O handler chama MPG_Validators::nonce_check() antes de processar. Parece seguro — até analisar o que esse check faz para usuários não autenticados:
// MPG_Validators::nonce_check() — trecho simplificado
if (is_user_logged_in()) {
// capability check aqui
current_user_can(...);
}
// para não autenticados: apenas valida o nonce, sem capability check
check_ajax_referer(MPG_BASENAME, 'securityNonce');
O bloco de capability check só existe dentro do if (is_user_logged_in()). Para um visitante anônimo, a única barreira é o nonce — e o nonce está disponível para qualquer um.
3. O nonce que não é barreira
A função mpg_front_assets_enqueue() em Helper.php enfileira assets e localiza um nonce quando a página atual é de busca:
// Helper.php — mpg_front_assets_enqueue()
if (is_search()) {
wp_localize_script(..., ['securityNonce' => wp_create_nonce(MPG_BASENAME)]);
}
Toda vez que qualquer visitante acessa a página de resultados de busca do site (/?s=qualquer_coisa), o nonce é impresso no HTML. Não é necessário estar logado. Não é necessário ter uma conta. Um GET simples é suficiente para obtê-lo:
# Raspar nonce como usuário anônimo
curl -s 'http://HOST/?s=x' | grep -oE 'securityNonce":"[a-f0-9]+'
# → securityNonce":"05623873b1"
Este é o mesmo padrão da CVE-2026-12582: nonce exposto em página pública não é barreira de autorização. O token protege contra CSRF de terceiros, mas não contra um atacante que simplesmente lê o HTML da página. Quando o nonce está disponível para uid=0, a proteção que ele oferece é zero para o cenário de acesso não autenticado.
4. O sink: echo sem encode
Com o nonce em mãos, o atacante pode chamar o handler. O parâmetro content é lido cru de $_POST e usado como template de renderização:
// controllers/CoreController.php:249,274
$content = $_POST['content']; // linha 249, sem sanitize
$results = $inline->render($project_id, [...], $content); // content como template
echo '{"success": true, "data":"' . str_replace("\n",'<br>',$results) . '"}'; // sem esc/JSON-encode
O método render() em loop/Core.php:142 substitui placeholders como [mpg_*], mas preserva todo o restante do content intacto. O HTML arbitrário passa direto para o echo.
O problema no echo é duplo: além de não escapar para HTML, ele concatena a string manualmente em vez de usar wp_json_encode(). Como a resposta é servida como HTML (veja a seguir), o markup do content é refletido cru e interpretado como parte do documento — o payload nem precisa “quebrar” a estrutura JSON.
O Content-Type da resposta é text/html (padrão do admin-ajax.php sem override), então o browser interpreta o markup refletido como HTML executável.
| Barreira esperada | Status | Observação |
|---|---|---|
Nonce (check_ajax_referer) | Contornável | Nonce vazado em /?s=x — anônimo obtém token válido |
| Capability check | Ausente para anônimos | Check só existe dentro de is_user_logged_in() |
Sanitização do content | Ausente | $_POST['content'] lido sem sanitize_text_field ou similar |
| Encode na saída | Ausente | echo concatena cru — sem wp_json_encode ou esc_html |
5. A pré-condição (e por que ela não salva)
O handler exige um projectId válido — ou seja, um projeto MPG com dataset deployado deve existir no site. Alguém poderia argumentar que isso é uma mitigação efetiva.
Não é. Três motivos:
- Pré-condição de uso normal: qualquer site que efetivamente usa o plugin — o caso de 100% dos 2.000 sites ativos — tem pelo menos um projeto deployado. A pré-condição está sempre satisfeita em ambientes reais.
- Project ID é sequencial e público: os IDs são números inteiros atribuídos em ordem (1, 2, 3...). Um atacante testa
projectId=1e, se não funcionar, incrementa. Não há segredo a descobrir. - O erro também reflete: em alguns casos, erros de projeto inválido também retornam conteúdo parcial com o
contentoriginal — confirmamos comportamento misto no lab.
6. A correção certa
A correção exige três mudanças independentes, e qualquer uma delas quebra a cadeia:
1. Usar wp_json_encode no echo
// CoreController.php — ANTES (vulnerável)
echo '{"success": true, "data":"' . str_replace("\n",'<br>',$results) . '"}';
// DEPOIS (corrigido)
wp_send_json_success($results); // ou: echo wp_json_encode(['success'=>true,'data'=>$results]);
wp_send_json_success usa wp_json_encode internamente, que aplica JSON_HEX_TAG — converte < e > em sequências de escape Unicode — sem </>, nenhuma tag HTML sobrevive, mesmo com a resposta servida como text/html.
2. Adicionar esc_html no content
// CoreController.php — sanitizar antes de passar para render
$content = sanitize_textarea_field( $_POST['content'] );
3. Verificar capability também para não autenticados
// MPG_Validators::nonce_check() — DEPOIS
check_ajax_referer(MPG_BASENAME, 'securityNonce');
if (!is_user_logged_in() || !current_user_can('edit_posts')) {
wp_send_json_error(['message' => 'Forbidden'], 403);
wp_die();
}
O princípio: JSON deve ser gerado com serialização segura, não com concatenação de string. Concatenar HTML dentro de aspas duplas JSON é uma receita garantida de XSS.
wp_send_json_success()existe exatamente para evitar esse erro.
7. Disclosure e timeline
- 29/06/2026 — Descoberta e confirmação em laboratório (rodada overnight, iteração 3). PoC completo: raspar nonce em
/?s=x, chamarmpg_shortcodecomprojectId=1econtentarbitrário. - 29/06/2026 — Prior-art confirmado: WPScan lista 12 vulnerabilidades históricas no MPG; nenhuma relacionada ao handler
mpg_shortcode/ parâmetrocontentnas versões 4.x. Achado inédito. Submetido ao WPScan; publicado como CVE-2026-13726. - Versão 4.1.8 — correção esperada. Atualize assim que disponível.
- Após patch — liberação do script de automação e do payload completo.
Se você usa o Multiple Page Generator: até a 4.1.8 ser liberada, desative o plugin em ambientes onde administradores tenham sessões ativas, especialmente se o site tiver a busca habilitada (o que vaza o nonce). A desativação do campo de busca público não é mitigação suficiente por si só — outros caminhos de enqueue podem expor o nonce.
Este caso ilustra bem um padrão recorrente: duas falhas de gravidade individual moderada que, combinadas, formam um vetor não autenticado. A análise de causa raiz exige entender cada peça — a lógica de enqueue do nonce, o fluxo do handler, o comportamento do echo — e como elas se encaixam. É esse nível de profundidade que a TreeSec aplica nos testes dos nossos clientes.
Sua aplicação tem handlers AJAX expostos como este?
Nossos pentests Web & API mapeiam superfícies AJAX, verificam nonces, capability checks e encode de saída — exatamente o que encontrou esta falha.