CVE-2026-16618 CVSS 10.0 · Crítico
Software: ImproveSEO (plugin WordPress) · slug improveseo
Versões afetadas: ≤ 2.0.11 (a 2.0.11 é a última versão publicada) · sem correção disponível — o plugin foi fechado no repositório do WordPress.org em 23/07/2026.
Classe: Upload de arquivo arbitrário não autenticado (CWE-434) → Execução Remota de Código
Vetor: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
Privilégio necessário: nenhum (não autenticado) · Crédito: Equipe TreeSec
Validado em laboratório: 19 de julho de 2026 · Publicado no WPScan em 24/07/2026
Referências: WPScan · WordPress.org
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. Como não há correção disponível, o payload final e o script de automação estão deliberadamente omitidos deste artigo.
Neste write-up
1. Por que file upload é o pior caso
Entre as classes de vulnerabilidade web, upload de arquivo arbitrário sem autenticação é a que menos exige do atacante e a que mais entrega. Não há sessão para roubar, nonce para forjar ou vítima para enganar: o atacante fala direto com o servidor, grava um arquivo .php dentro do webroot e passa a executar código arbitrário no contexto do processo PHP. A partir daí é servidor comprometido — leitura do wp-config.php, credenciais do banco, pivô para a rede interna.
É por isso que o WPScan classificou este achado em CVSS 10.0, a nota máxima. O vetor é AV:N/AC:L/PR:N/UI:N (rede, baixa complexidade, sem privilégio, sem interação) com escopo alterado e impacto total em confidencialidade, integridade e disponibilidade. Não existe combinação mais grave.
O detalhe que torna este caso didático é por que a proteção falha. O plugin não esqueceu de validar o upload — ele valida. Só que valida a coisa errada.
2. A superfície: handler nopriv + nonce comentado
O ImproveSEO registra um handler AJAX de upload e o expõe também para usuários não autenticados, no topo do arquivo — fora de qualquer bloco !is_admin():
// improveseo.php:1071
add_action('wp_ajax_my_plugin_upload', 'my_plugin_handle_upload');
// improveseo.php:1073
add_action('wp_ajax_nopriv_my_plugin_upload', 'my_plugin_handle_upload'); // ← anônimo chega aqui
Com o gancho nopriv, qualquer visitante alcança o handler por /wp-admin/admin-ajax.php. Restaria o nonce como barreira — mas ele está comentado:
// improveseo.php:1001 — dentro de my_plugin_handle_upload()
// check_ajax_referer('my-plugin-nonce', '_wpnonce'); ← desativado
Não há check_ajax_referer() ativo e não há current_user_can(). O handler é integralmente não autenticado: sem token, sem capability, sem sessão. A única coisa entre o atacante e o disco é a validação do arquivo — e é aí que mora o bug.
3. A validação que não valida
O handler faz exatamente uma checagem de conteúdo antes de gravar:
// improveseo.php:1021 — identifica o tipo pelo CONTEÚDO do arquivo
$fileType = mime_content_type($tmpName);
// :1025 — só aceita imagem
// whitelist: ['image/jpeg', 'image/png', 'image/gif']
// :1027 — monta o nome final. sanitize_file_name NÃO remove ".php"
$filename = uniqid() . '_' . sanitize_file_name($name);
$uploadPath = wp_upload_dir()['path']; // webroot, executa PHP
O erro é escolher mime_content_type() como autoridade. Essa função (libmagic) decide o tipo lendo os bytes iniciais do arquivo — os “números mágicos” —, não a extensão. E o nome final é montado com sanitize_file_name(), que higieniza caracteres perigosos do nome mas preserva a extensão: shell.php continua shell.php.
Ou seja: a decisão de “isto é seguro?” é tomada pelos primeiros bytes, mas o que determina se o arquivo executa é a extensão. São dois critérios independentes — e o atacante controla os dois.
4. O polyglot GIF/PHP
A ponte entre os dois critérios é um polyglot: um arquivo que é, ao mesmo tempo, um GIF válido para o libmagic e um script PHP válido para o interpretador. Basta que ele comece com a assinatura de um GIF e, em seguida, contenha código PHP:
# Estrutura conceitual do arquivo (payload real omitido)
GIF89a;<?php /* código do atacante */ ?>
Os bytes GIF89a; fazem o mime_content_type() devolver image/gif — passa na whitelist. Mas o arquivo se chama shell.php, e o PHP não liga para os bytes iniciais: ao receber uma requisição para shell.php, ele ignora o cabeçalho GIF (que é só texto solto para o parser) e executa tudo que estiver entre <?php ... ?>.
É o mesmo princípio de qualquer bypass de upload por “magic bytes”: validar o conteúdo não substitui validar a extensão. Um arquivo pode satisfazer o formato de imagem e ainda ser um programa. Quem só olha os bytes está checando se parece uma imagem, não se é seguro executar.
5. move_uploaded_file: por que fura o WordPress
O WordPress tem defesa nativa contra exatamente isto. A função wp_handle_upload() passa o arquivo por wp_check_filetype_and_ext() e por uma blocklist de extensões executáveis — .php é barrado ali, independentemente do MIME. O problema é que o plugin não usa nada disso. Ele grava o arquivo na mão:
// improveseo.php:1033 — grava direto, sem passar pelo core
move_uploaded_file($tmpName, $filePath);
// :1035 — e devolve a URL pública do arquivo salvo
wp_send_json_success([$publicUrl]);
move_uploaded_file() é uma primitiva pura do PHP: move o arquivo temporário para o destino, ponto. Não conhece a blocklist do WordPress, não valida extensão, não sanitiza nada além do que já veio. Ao trocar wp_handle_upload() por move_uploaded_file(), o plugin abriu mão de toda a proteção do core — e ainda entrega ao atacante, na resposta JSON, a URL exata onde o arquivo foi parar. Não é preciso adivinhar o caminho: o servidor informa.
| Barreira esperada | Status | Observação |
|---|---|---|
Autenticação / nopriv | Ausente | Handler registrado também em wp_ajax_nopriv_ — anônimo alcança |
Nonce (check_ajax_referer) | Comentado | Linha 1001 desativada — nenhum token exigido |
| Capability check | Ausente | Sem current_user_can() em nenhum ponto do handler |
| Validação de extensão | Contornável | Decide por mime_content_type() (bytes), extensão .php preservada |
| Blocklist do core | Ignorada | move_uploaded_file() em vez de wp_handle_upload() |
São cinco barreiras. Nenhuma delas está de pé. Basta uma para quebrar a cadeia — e é justamente por não haver nenhuma que a nota é 10.0.
6. Confirmado em laboratório
Validamos o achado em ambiente isolado (lab local, WordPress com ImproveSEO 2.0.11 sobre PHP 8.2.31), de forma não autenticada — sem cookie, sem nonce. O fluxo confirmado, em três passos:
- Enviar o polyglot para
action=my_plugin_uploadviaadmin-ajax.php, no campoimages[], comContent-Type: image/gif. - O servidor responde
{"success":true,...}com a URL do arquivo gravado em/wp-content/uploads/AAAA/MM/<uniqid>_shell.php. - Um
GETnessa URL retorna o resultado da execução do PHP no servidor.
7. Sem patch: o que fazer agora
Este é o ponto que mais importa para quem administra sites. O plugin foi fechado no WordPress.org em 23/07/2026, o que significa duas coisas: não há atualização para instalar, e sites que já o têm instalado continuam com o código vulnerável ativo — fechar um plugin no repositório não o desinstala de ninguém.
Se você usa o ImproveSEO, a única mitigação real é:
- Desative e remova o plugin imediatamente. Não há como configurar em volta da falha — o handler vulnerável é registrado sempre que o plugin está ativo.
- Cace artefatos. Procure arquivos
.phprecentes emwp-content/uploads/(o diretório de uploads não deveria conter PHP algum). Nomes no padrão<hex>_*.phpsão suspeitos. - Bloqueie execução de PHP em uploads no servidor web (regra de
.htaccess/nginx negando.phpsob/uploads/) — defesa em profundidade que teria neutralizado esta falha por completo. - Se houve exposição, trate como comprometimento: rotacione as credenciais do
wp-config.php, revise usuários administrativos e investigue a partir dos logs de acesso aoadmin-ajax.php.
8. A correção certa
Do lado do desenvolvedor, a correção não é ajustar a whitelist de MIME — é parar de decidir por MIME. Quatro mudanças, e qualquer uma quebra a cadeia:
1. Usar o pipeline de upload do WordPress
// Deixa o core validar extensão e aplicar a blocklist
$upload = wp_handle_upload($_FILES['images'], ['test_form' => false]);
if (isset($upload['error'])) { wp_send_json_error($upload['error']); }
2. Validar extensão E tipo, não só o conteúdo
$check = wp_check_filetype_and_ext($tmpName, $name);
if (! in_array($check['ext'], ['jpg','jpeg','png','gif'], true)) {
wp_send_json_error('Extensão não permitida', 400);
}
3. Reativar nonce e exigir capability
check_ajax_referer('my-plugin-nonce', '_wpnonce');
if (! current_user_can('upload_files')) {
wp_send_json_error('Forbidden', 403);
}
E, no servidor, a regra de negar execução de PHP no diretório de uploads — que transforma qualquer upload malicioso, presente ou futuro, em um arquivo inerte.
O princípio: a extensão é o que decide se um arquivo executa; portanto é a extensão que precisa ser validada. Checar o conteúdo (magic bytes) responde “isto se parece com uma imagem?”, não “é seguro gravar isto com este nome?”. E
wp_handle_upload()existe precisamente para você não ter que acertar essa distinção na mão.
9. Disclosure e timeline
- 19/07/2026 — Descoberta e confirmação em laboratório (WordPress + ImproveSEO 2.0.11, PHP 8.2.31). Prior-art verificado: o WPScan listava 0 vulnerabilidades para o plugin — achado inédito, sem colisão.
- Julho/2026 — Submetido ao WPScan pela Equipe TreeSec, com pedido de CVE.
- 23/07/2026 — Plugin fechado no repositório do WordPress.org, pendente de revisão.
- 24/07/2026 — Publicado como CVE-2026-16618, CVSS 10.0. Sem correção disponível até a data.
Se você usa o ImproveSEO: remova o plugin hoje. Como não há versão corrigida e o repositório já não o distribui, não existe “atualizar e seguir” — a mitigação é a desinstalação, seguida de varredura do diretório de uploads.
Este caso resume um padrão que encontramos repetidamente: a defesa existia no framework, e o código a contornou “na mão” — um move_uploaded_file() no lugar de um wp_handle_upload(), um nonce comentado “só para testar”. Achar isso não é rodar um scanner; é ler o fluxo do handler, entender o que cada função realmente valida e onde a cadeia se rompe. É esse nível de profundidade que a TreeSec aplica nos testes dos nossos clientes.
Sua aplicação aceita uploads de usuário?
Nossos pentests Web & API testam cada ponto de upload contra bypass de MIME, polyglots, path traversal e execução no webroot — exatamente a cadeia que virou esta CVE 10.0.