← Voltar para Insights Write-up · XSS Não Autenticado

MPG Plugin < 4.1.8: o nonce que vaza na busca e o echo que executa tudo

O plugin Multiple Page Generator expõe um nonce válido para qualquer visitante anônimo na página de resultados de busca. Com esse token, o handler mpg_shortcode aceita content arbitrário e o ecoa sem encode — XSS confirmado no 4.1.7, sem cookie, em duas requisições.

Identificação

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.

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 esperadaStatusObservação
Nonce (check_ajax_referer)ContornávelNonce vazado em /?s=x — anônimo obtém token válido
Capability checkAusente para anônimosCheck só existe dentro de is_user_logged_in()
Sanitização do contentAusente$_POST['content'] lido sem sanitize_text_field ou similar
Encode na saídaAusenteecho 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:

  1. 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.
  2. Project ID é sequencial e público: os IDs são números inteiros atribuídos em ordem (1, 2, 3...). Um atacante testa projectId=1 e, se não funcionar, incrementa. Não há segredo a descobrir.
  3. O erro também reflete: em alguns casos, erros de projeto inválido também retornam conteúdo parcial com o content original — confirmamos comportamento misto no lab.

Script de automação e payload final sob divulgação coordenada

O PoC de duas requisições (raspar nonce + injetar payload) foi confirmado no lab com PHP 8.2 e MPG 4.1.7. O script completo e o payload content utilizados estão deliberadamente omitidos deste artigo enquanto a versão 4.1.8 não tiver penetração suficiente nos sites ativos.

Confirmado em laboratório: resposta {"success": true, "data":"[payload refletido cru]..."} com execução em browser. Sem cookie. Sem login.

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

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.