CVE-2026-12982 CVSS 7.1 · Alto
Software: Document Gallery (plugin WordPress) · slug document-gallery
Versões afetadas: < 5.1.1 (testado no 5.1.0, last_updated 2025-12-09) · Active installs: ~8.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 · publicado no WPScan
Referências: WPScan
Aviso legal: toda a validação foi feita exclusivamente em laboratório próprio (WordPress em Docker), de forma não destrutiva. Nenhum sistema de terceiros foi testado ou afetado. O PoC completo e o script de automação estão sob divulgação coordenada — os payloads finais não estão incluídos neste artigo.
Neste write-up
1. Por que um XSS não autenticado importa
XSS é frequentemente minimizado: “precisa de interação do usuário, não é tão grave.” Essa leitura é errada quando o vetor é não autenticado e entregável por GET. Um atacante não precisa de conta, não precisa de nonce, não precisa de engenharia social elaborada — basta um link. E links chegam por e-mail, mensagem, QR code.
O impacto sobe de patamar quando a vítima é um administrador logado: sessão sequestrada, painel do WordPress comprometido, conteúdo alterado. Em 8.000 instalações ativas, a probabilidade de uma delas estar vulnerável e com um admin navegando ao mesmo tempo não é desprezível — é uma questão de escala.
Reflected XSS (OWASP A03) é o tipo mais comum de XSS e, historicamente, o mais fácil de explorar via phishing direcionado. O que diferencia este achado do ruído diário de CVEs é a combinação: handler anônimo + Content-Type text/html + ausência total de escape na renderização do erro.
2. A superfície: handler AJAX aberto sem nonce
O plugin Document Gallery gerencia galerias de documentos dentro do WordPress por meio do shortcode [dg]. Para renderizar pré-visualizações, ele expõe um handler AJAX dedicado — e o registra tanto para usuários autenticados quanto para visitantes anônimos:
// src/admin/class-ajax-handler.php
add_action('wp_ajax_dg_generate_gallery', array('DG_AjaxHandler','generateGallery'));
add_action('wp_ajax_nopriv_dg_generate_gallery', array('DG_AjaxHandler','generateGallery')); // ← anônimo
public static function generateGallery() {
if (isset($_REQUEST['atts'])) {
if (!headers_sent()) {
header('Content-Type: text/html; charset='.get_bloginfo('charset')); // text/html!
}
echo DocumentGallery::doShortcode($_REQUEST['atts']); // $_REQUEST['atts'] array
}
wp_die();
}
Três problemas imediatos neste código:
- Sem
check_ajax_referer: não há verificação de nonce. Qualquer requisição ao endpoint/wp-admin/admin-ajax.php?action=dg_generate_galleryé processada. - Sem capability check: não há
current_user_can(). Visitante anônimo e administrador recebem o mesmo tratamento. - Content-Type
text/html: a resposta é declarada explicitamente como HTML. Se qualquer fragmento da saída contiver markup executável, o browser o interpreta.
O array $_REQUEST['atts'] é passado diretamente para DocumentGallery::doShortcode(), que o repassa ao construtor de DG_Gallery. A partir daí, os atributos são validados — ou melhor, deveriam ser.
3. O caminho até o sink: erro que não escapa
O construtor de DG_Gallery chama sanitizeDefaults(), que por sua vez invoca DG_GallerySanitization::sanitizeParameter() para cada atributo. Para os atributos booleanos do shortcode — descriptions, fancy, new_window, attachment_pg, include_children — a validação espera literalmente "true" ou "false". Qualquer outro valor cai no ramo de erro:
// class-gallery-sanitization.php — ramo de erro para booleanos
self::$binary_err = __('The %s parameter may only be "%s" or "%s." You entered "%s."', ...);
$err = sprintf(self::$binary_err, 'descriptions', 'true', 'false', $value); // $value = input cru
O método toBool() em class-util.php:72 retorna null para qualquer string que não seja reconhecida como booleana — portanto, qualquer payload sempre cai neste ramo. O erro é armazenado em $this->errs e renderizado em __toString():
// class-gallery.php:469-472
public function __toString() {
if (!empty($this->errs)) {
return '<p>'.implode('</p><p>', $this->errs).'</p>'; // sem esc_html!
}
}
A cadeia completa: $_REQUEST['atts']['descriptions'] → sanitizeParameter() → sprintf($binary_err, ..., $value) → $this->errs[] → __toString() → echo → browser.
| Ponto de defesa esperado | Presente? | Motivo da falha |
|---|---|---|
Nonce (check_ajax_referer) | Ausente | Handler registrado sem qualquer verificação de referer/nonce |
Capability check (current_user_can) | Ausente | Nenhuma restrição de papel — anônimo acessa igual a admin |
Escape na saída (esc_html) | Ausente | __toString() concatena os erros sem escape |
| Content-Type seguro | Falha | Declarado explicitamente como text/html — browser executa o markup |
4. Provando a exploração (PoC parcial, drive-by GET)
O PoC foi confirmado em laboratório isolado (WordPress em Docker, sem acesso externo). O handler aceita tanto POST quanto GET — admin-ajax.php processa ambos via $_REQUEST. Isso transforma o XSS em um drive-by via link: nenhuma interação além de clicar em uma URL.
Via POST (curl, sem cookie)
# Confirmação de laboratório — POST anônimo
curl -s 'http://localhost:8080/wp-admin/admin-ajax.php' \
--data-urlencode 'action=dg_generate_gallery' \
--data-urlencode 'atts[descriptions]=PAYLOAD_AQUI'
# Resposta: <p>The descriptions parameter may only be "true" or "false." You entered "PAYLOAD_AQUI."</p>
A resposta reflete literalmente o valor fornecido dentro de um elemento <p>, com Content-Type: text/html. Qualquer tag HTML funciona. No lab, um marcador SVG sem alert() foi usado para confirmar a execução sem simular ataque real.
Drive-by via GET
Como $_REQUEST abrange GET, POST e COOKIE, o atacante pode construir uma URL completa que, ao ser aberta pelo navegador da vítima, executa o payload imediatamente. Não há formulário, não há POST, não há segundo clique.
Prior-art: pesquisa exaustiva no WPScan, Patchstack e NVD não retornou nenhum registro de XSS via dg_generate_gallery nas versões 4.x/5.x. Este achado é inédito.
5. A correção certa
A correção exige duas ações independentes — e qualquer uma delas, sozinha, já quebra a cadeia de ataque:
1. Escapar o valor na mensagem de erro
// class-gallery-sanitization.php — ANTES (vulnerável)
$err = sprintf(self::$binary_err, 'descriptions', 'true', 'false', $value);
// DEPOIS (corrigido)
$err = sprintf(self::$binary_err, 'descriptions', 'true', 'false', esc_html($value));
Uma chamada a esc_html() no quarto argumento do sprintf converte <, > e & em entidades HTML — o payload vira texto inerte na tela em vez de código executado.
2. Adicionar verificação de nonce no handler
// class-ajax-handler.php — DEPOIS (corrigido)
public static function generateGallery() {
check_ajax_referer('dg_generate_gallery_nonce', 'security'); // ← barreira de CSRF
if (isset($_REQUEST['atts'])) {
echo DocumentGallery::doShortcode($_REQUEST['atts']);
}
wp_die();
}
O nonce deve ser gerado no front-end (via wp_localize_script) e transmitido em cada chamada AJAX. Isso não impede o drive-by via GET de um usuário autenticado que já tenha o nonce, mas elimina qualquer uso não autenticado — o cenário de maior risco.
A regra geral: toda saída que vai para o HTML deve ser escapada no ponto de saída. Não na entrada, não no meio do caminho — no exato momento do
echo/nopriv_que devolva HTML deve ser tratado como superfície pública de ataque.
6. Disclosure e timeline
- 29/06/2026 — Descoberta e confirmação em laboratório (POST e GET, parâmetros
descriptions,fancy,new_window,attachment_pg). - 29/06/2026 — Prior-art confirmado como inédito (WPScan, Patchstack, NVD). Submetido ao WPScan; publicado como CVE-2026-12982.
- Versão 5.1.1 — correção esperada. Atualize assim que disponível.
- Após patch — liberação do PoC completo e URL de ataque.
Se você usa o Document Gallery: até a versão 5.1.1 estar disponível, considere desativar o plugin em sites que tenham administradores com sessão ativa — especialmente em sites que recebam links por e-mail ou mensagens de fontes externas.
Este achado é mais um resultado direto da metodologia de caça em escala da TreeSec: varredura estática de mais de 13 mil plugins, filtro por alcançabilidade anônima, triagem humana da causa raiz. O mesmo processo que gera CVEs públicas é o que aplicamos nos pentests dos nossos clientes — porque encontrar a falha certa exige entender o código, não só rodar ferramentas.
Sua aplicação WordPress está protegida contra este tipo de falha?
Nossos pentests Web & API identificam XSS, injeções e falhas de autorização que scanners automatizados não detectam — incluindo handlers AJAX expostos como este.